Panduan QA Testing - Scola LMS E2E dengan Playwright¶
Last Updated: 2026-01-26
Context: Pembelajaran dari Phase-1 (27 tests) dan Phase-2.1 (12 tests)
Status: Production-Ready Guidelines
🎯 Prinsip Utama: "Test Against Reality, Not Assumptions"¶
❌ Kesalahan Umum yang Harus Dihindari¶
- Asumsi URL routing tanpa verifikasi
- ❌ SALAH: Mengasumsikan
/teacher/hometanpa cek kode -
✅ BENAR: Cek
router.jsatau test manual di browser terlebih dahulu -
Asumsi selector tanpa inspeksi DOM
- ❌ SALAH: Mengasumsikan
data-testid="submit-button"ada -
✅ BENAR: Inspect element di browser dev tools, lihat attribute actual
-
Asumsi login flow tanpa test API
- ❌ SALAH: Langsung buat test Playwright dengan asumsi flow
-
✅ BENAR: Test login via
curldulu, pahami response structure -
Tidak mengecek prerequisite sebelum run
- ❌ SALAH: Langsung
npx playwright testtanpa cek backend/frontend - ✅ BENAR: Cek
ps aux | grep odoo-bindanps aux | grep vite
📋 Checklist Wajib Sebelum Membuat Test Baru¶
1. Verifikasi Environment (5 menit)¶
# A. Cek backend running
ps aux | grep odoo-bin
# HARUS ADA: python3 odoo-bin --config odoo-xxx.conf
# B. Cek frontend running
ps aux | grep vite
# HARUS ADA: node .../vite/bin/vite.js (port 5173)
# C. Cek database seeded
cd /home/scola/odoo
make qa-reset && make qa-seed
# HARUS KELUAR: "QA users created successfully"
# D. Test manual login via API
curl -X POST http://127.0.0.1:8069/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"teacher1","password":"teacher123"}'
# HARUS DAPAT: {"success":true,"user":{...},"role":"teacher"}
⏱️ Waktu: 5 menit
Jika gagal: STOP! Fix environment dulu, jangan lanjut buat test
2. Inspect Actual Frontend Code (10 menit)¶
A. Cek Routing (File: src/router/index.js atau src/router.js)¶
# Cari route patterns
grep -r "path.*teacher" scola-fe-v2/src/router/
grep -r "path.*student" scola-fe-v2/src/router/
grep -r "path.*parent" scola-fe-v2/src/router/
# Output actual akan memberi tahu:
# - /faculty/home (bukan /teacher/home!)
# - /student/dashboard (bukan /student/home!)
# - /parent/academic (bukan /parent/grades!)
Lesson Learned:
// ❌ ASUMSI (SALAH!)
await page.waitForURL('/teacher/home')
// ✅ DARI KODE ACTUAL (BENAR!)
await page.waitForURL('/faculty/home')
B. Cek Login Component Selectors (File: src/views/Login.vue atau src/components/Login.jsx)¶
# Lihat file login component
cat scola-fe-v2/src/views/Login.vue | grep -A5 "input.*username"
cat scola-fe-v2/src/views/Login.vue | grep -A5 "input.*password"
cat scola-fe-v2/src/views/Login.vue | grep -A5 "button.*login"
Output akan memberi tahu attribute actual:
<!-- Yang kita dapat mungkin: -->
<input v-model="username" class="form-control" placeholder="Username">
<input v-model="password" type="password" class="form-control">
<button @click="handleLogin" class="btn btn-primary">Masuk</button>
<!-- Bukan yang diasumsikan: -->
<input data-testid="username"> <!-- TIDAK ADA! -->
Lesson Learned:
// ❌ ASUMSI (SALAH!)
await page.fill('[data-testid="username"]', 'teacher1')
// ✅ DARI INSPEKSI ACTUAL (BENAR!)
await page.fill('input[placeholder="Username"]', 'teacher1')
// ATAU jika v-model visible:
await page.fill('input.form-control:near(label:has-text("Username"))', 'teacher1')
C. Cek API Service (File: src/services/auth/auth.service.js)¶
# Lihat endpoint login
grep -r "api/auth/login" scola-fe-v2/src/
# Lihat response handling
cat scola-fe-v2/src/services/auth/auth.service.js | grep -A10 "login"
Output akan memberi tahu:
- Endpoint: /api/auth/login (bukan /auth/login)
- Response structure: {success, user, role, token}
- Redirect logic: router.push('/faculty/home') untuk teacher
3. Test Manual di Browser Terlebih Dahulu (5 menit)¶
# Buka browser
xdg-open http://localhost:5173/login
# Manual steps:
# 1. Buka DevTools (F12)
# 2. Buka tab Network
# 3. Login dengan teacher1/teacher123
# 4. Perhatikan:
# - Request URL: POST /api/auth/login
# - Response: {success: true, role: "teacher"}
# - Redirect URL: /faculty/home (BUKAN /teacher/home!)
# 5. Buka tab Elements
# 6. Inspect form inputs - lihat selector actual
⏱️ Waktu: 5 menit
Output: Screenshot atau note tentang URL/selector actual
4. Test API Backend Langsung (3 menit)¶
# Test teacher login
curl -X POST http://127.0.0.1:8069/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"teacher1","password":"teacher123"}' \
| jq '.'
# Expected response:
{
"success": true,
"user": {
"id": 123,
"name": "Teacher One",
"username": "teacher1"
},
"role": "teacher"
}
# Test student login
curl -X POST http://127.0.0.1:8069/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"student1","password":"student123"}' \
| jq '.'
Jika dapat "success": false:
- Database belum di-seed
- Credentials salah
- Backend belum running
- STOP dan fix ini dulu!
🔧 Pattern Yang Sudah Terbukti Bekerja¶
Auth Helper dengan API Login (Bukan UI Login)¶
Lesson Learned: UI login sering gagal karena Vite proxy, CORS, atau timing. Gunakan API langsung!
// ✅ BEST PRACTICE (dari troubleshooting Phase-2.1)
export async function loginAsTeacher(page: Page): Promise<void> {
// 1. Login via API backend langsung (bypass UI issues)
const response = await fetch('http://127.0.0.1:8069/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: CREDENTIALS.teacher.username,
password: CREDENTIALS.teacher.password
})
})
const data = await response.json()
if (!data.success) {
throw new Error(`Login failed: ${data.error || 'Unknown error'}`)
}
// 2. Set cookies dari response di Playwright context
const cookies = response.headers.get('set-cookie')
if (cookies) {
await page.context().addCookies([
{
name: 'session_id',
value: extractSessionId(cookies),
domain: 'localhost',
path: '/'
}
])
}
// 3. Navigate ke halaman yang BENAR (dari router.js, bukan asumsi!)
await page.goto('/faculty/home') // ACTUAL dari router!
await page.waitForLoadState('networkidle')
// 4. Save session untuk reuse
await page.context().storageState({
path: CREDENTIALS.teacher.storageFile
})
}
Mengapa Cara Ini Lebih Baik: - ✅ Tidak bergantung pada selector UI yang berubah - ✅ Tidak masalah CORS/proxy Vite - ✅ Lebih cepat (skip UI interaction) - ✅ Lebih stabil (API contract lebih stabil dari UI)
Selector Strategy dengan Fallback Bertingkat¶
// ✅ BEST PRACTICE
const gradeInput = page.locator('[data-testid="lms-grade-input"]') // Priority 1: Semantic
.or(page.locator('input[type="number"][placeholder*="grade"]')) // Priority 2: Attribute
.or(page.locator('input[name*="score"]')) // Priority 3: Name
.or(page.locator('.grade-input')) // Priority 4: Class
.first()
// Jika TIDAK ditemukan:
const isVisible = await gradeInput.isVisible({ timeout: 2000 }).catch(() => false)
if (!isVisible) {
console.warn('⚠️ Grade input not found - UI may not be implemented yet')
test.skip() // Skip test, jangan fail
}
Mengapa: - ✅ Frontend masih berkembang, UI belum final - ✅ Warning lebih baik dari hard failure - ✅ Test tetap bisa dijalankan meski UI incomplete
Wait Strategy untuk SPA Navigation¶
// ❌ SALAH: Tidak wait
await page.click('[data-testid="course-card"]')
await page.click('[data-testid="assignment-item"]') // GAGAL! Belum load
// ✅ BENAR: Wait network idle setelah navigation
await page.click('[data-testid="course-card"]')
await page.waitForLoadState('networkidle') // WAJIB untuk SPA!
await page.click('[data-testid="assignment-item"]')
🚨 Troubleshooting Checklist Cepat¶
Issue: "Timeout waiting for element"¶
# 1. Cek apakah element benar-benar ada di UI
npx playwright test --headed --debug tests/e2e/flow/xxx.spec.ts
# 2. Pause di breakpoint, inspect DOM
# Di test tambahkan:
await page.pause()
# 3. Screenshot untuk debug
await page.screenshot({ path: 'debug-state.png', fullPage: true })
Issue: "Login failed: Access Denied"¶
# 1. Test API langsung
curl -X POST http://127.0.0.1:8069/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"teacher1","password":"teacher123"}'
# Jika dapat {"success": false, "error": "Access Denied"}:
# - Database belum di-seed
cd /home/scola/odoo && make qa-reset && make qa-seed
# 2. Verifikasi user ada di database
docker exec -it odoo_db psql -U odoo -d scola_test -c "SELECT username, active FROM res_users WHERE login='teacher1';"
Issue: "Navigation timeout"¶
# 1. Cek routing actual di kode
grep -r "path.*faculty" scola-fe-v2/src/router/
# 2. Update test dengan URL yang benar
# ❌ await page.waitForURL('/teacher/home')
# ✅ await page.waitForURL('/faculty/home')
# 3. Gunakan pattern matching jika URL bervariasi
await page.waitForURL(/\/(faculty|teacher)\/home/)
📦 Template Test Baru (Copy-Paste Ready)¶
/**
* Test: [DESKRIPSI SINGKAT]
*
* Prerequisites Verified:
* - ✅ Backend running (ps aux | grep odoo-bin)
* - ✅ Frontend running (ps aux | grep vite)
* - ✅ Database seeded (make qa-seed)
* - ✅ API login tested (curl POST /api/auth/login)
* - ✅ Router.js checked for actual URLs
* - ✅ Login.vue inspected for selectors
* - ✅ Manual browser test completed
*
* Known Issues:
* - [Jika ada gap UI, catat di sini]
*/
import { test, expect } from '@playwright/test'
import { loginAsTeacher } from '../helpers/auth'
test.describe('[FEATURE NAME]', () => {
test.describe.configure({ retries: 0 }) // Deterministic only
test('[TEST-ID] @priority @tag Description', async ({ page }) => {
// 1. Setup - Login with API-based auth (proven stable)
await loginAsTeacher(page)
// 2. Navigate - Use ACTUAL URL from router.js
await page.goto('/faculty/gradebook') // VERIFIED in router.js!
await page.waitForLoadState('networkidle') // CRITICAL for SPA!
// 3. Interact - Use fallback selectors
const gradeInput = page.locator('[data-testid="grade-input"]')
.or(page.locator('input[type="number"]'))
.first()
// 4. Verify element exists before interact
const isVisible = await gradeInput.isVisible({ timeout: 5000 })
.catch(() => false)
if (!isVisible) {
console.warn('⚠️ UI element not found - feature may not be implemented')
test.skip()
}
// 5. Perform action
await gradeInput.fill('85')
// 6. Wait for response (if API call triggered)
await page.waitForResponse(resp =>
resp.url().includes('/api/lms/grades') && resp.status() === 200
)
// 7. Assert outcome
await expect(gradeInput).toHaveValue('85')
})
})
📊 Metrics dari Pembelajaran Phase-1 & Phase-2.1¶
| Kesalahan | Frekuensi | Waktu Troubleshoot | Solusi |
|---|---|---|---|
| Asumsi URL routing salah | 5x | ~30 menit | Cek router.js terlebih dahulu |
| Asumsi selector salah | 8x | ~45 menit | Inspect DOM di browser manual |
| Login timeout | 3x | ~60 menit | Gunakan API login, bukan UI |
| Element not found | 12x | ~90 menit | Fallback selectors + skip jika UI incomplete |
| Network timing issues | 4x | ~20 menit | waitForLoadState('networkidle') |
Total Waktu Terbuang: ~4 jam
Waktu Jika Ikut Panduan Ini: ~20 menit
🎓 Golden Rules untuk QA Engineer¶
Rule 1: "Inspect First, Code Later"¶
Jangan menulis satu baris test sebelum: - ✅ Lihat kode router - ✅ Inspect DOM di browser - ✅ Test API dengan curl - ✅ Manual test flow di browser
Waktu investasi: 15 menit
Waktu saved: 2-3 jam troubleshooting
Rule 2: "API Login > UI Login"¶
UI login rentan: - ❌ Selector berubah - ❌ CORS/proxy issues - ❌ Timing issues
API login stabil: - ✅ Contract tidak berubah - ✅ Langsung ke backend - ✅ Lebih cepat
Rule 3: "Fallback Always"¶
Jangan pernah percaya selector tunggal:
// ❌ Fragile
await page.click('[data-testid="submit"]')
// ✅ Robust
await page.locator('[data-testid="submit"]')
.or(page.locator('button:has-text("Submit")'))
.or(page.locator('button.btn-submit'))
.click()
Rule 4: "Skip > Fail"¶
Jika UI belum ada:
// ❌ Hard fail (block CI/CD)
await expect(element).toBeVisible() // CRASH!
// ✅ Graceful skip (warn only)
const exists = await element.isVisible({ timeout: 2000 }).catch(() => false)
if (!exists) {
console.warn('⚠️ UI incomplete')
test.skip()
}
Rule 5: "Verify Environment First"¶
Sebelum run test:
# 5-menit checklist
ps aux | grep odoo-bin # Backend?
ps aux | grep vite # Frontend?
make qa-seed # Database seeded?
curl POST /api/auth/login # API working?
Jika salah satu gagal: STOP! Fix dulu!
📁 File Structure Best Practice¶
scola-fe-v2/
├── tests/e2e/
│ ├── helpers/
│ │ └── auth.ts # API-based login (STABLE!)
│ ├── fixtures/
│ │ └── test-data.json # Deterministic test data
│ ├── smoke/ # Fast sanity checks
│ ├── critical/ # Business-critical paths
│ ├── flow/ # End-to-end user journeys
│ └── setup-auth.spec.ts # Auth verification
├── .auth/ # Session storage (gitignored)
│ ├── teacher.json
│ ├── student.json
│ └── parent.json
└── playwright.config.ts # Shared config
🔗 Integration dengan Development Workflow¶
Workflow untuk Frontend Developer¶
# 1. Buat feature baru di UI
# 2. Tambahkan data-testid attributes
<button data-testid="submit-assignment" @click="handleSubmit">
Submit
</button>
# 3. Update docs/UI_TESTID_REGISTRY.md
| Component | data-testid | Purpose |
|-----------|-------------|---------|
| Assignment Submit | submit-assignment | Student submits work |
# 4. Notify QA engineer
# 5. QA engineer buat test dengan selector yang sudah pasti ada
Workflow untuk QA Engineer¶
# 1. Terima notif dari frontend dev
# 2. Cek UI_TESTID_REGISTRY.md
# 3. Verify di browser manual (F12 > Elements)
# 4. Buat test dengan selector yang VERIFIED
# 5. Run test
# 6. Jika gagal, SCREENSHOT + LOG ke frontend dev
📚 Resources¶
Quick Reference Commands¶
# Environment check
cd /home/scola/odoo
ps aux | grep -E "(odoo-bin|vite)"
# Database reset
make qa-reset && make qa-seed
# Test API
curl -X POST http://127.0.0.1:8069/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"teacher1","password":"teacher123"}' | jq
# Inspect routing
grep -r "path.*teacher\|path.*student\|path.*parent" scola-fe-v2/src/router/
# Inspect selectors
grep -r "data-testid" scola-fe-v2/src/
# Run tests
cd /home/scola/odoo/scola-fe-v2
npx playwright test tests/e2e/flow --headed --debug
# View report
npx playwright show-report
Documentation Links¶
- AGENTS.md - AI agent development guidelines
- AGENTIC_AI_APP_DEV_GUIDE.md - Detailed development guide
- PHASE1_P0_IMPLEMENTATION_SUMMARY.md - Phase-1 learnings
- PHASE2_FLOW_IMPLEMENTATION_SUMMARY.md - Phase-2.1 learnings
- LAPORAN_EKSEKUSI_PHASE2_FLOW.md - Execution results
✅ Pre-Flight Checklist Sebelum Buat Test Baru¶
Print dan tempel di monitor:
☐ Backend running? (ps aux | grep odoo-bin)
☐ Frontend running? (ps aux | grep vite)
☐ Database seeded? (make qa-seed)
☐ API login tested? (curl POST /api/auth/login)
☐ Router.js checked? (grep -r "path.*" src/router/)
☐ Login.vue inspected? (cat src/views/Login.vue)
☐ Manual browser test? (xdg-open http://localhost:5173)
☐ Screenshot saved? (F12 > Elements > screenshot)
☐ Selectors documented? (docs/UI_TESTID_REGISTRY.md)
☐ Fallback strategy ready? (.or() locators)
Jika semua ☑️ → START CODING
Jika ada ☐ → JANGAN CODING, FIX DULU!
⚡ P0 Fast Path: Critical Flow Testing (2-5 Minutes)¶
Gunakan ini ketika: Perlu validasi cepat fitur LMS critical path (submit → grade → publish → view)
Setup (30 detik)¶
cd /home/scola/odoo/scola-fe-v2
# Verify environment
ps aux | grep -E "(odoo-bin|vite)" | grep -v grep || echo "⚠️ Services not running!"
# Reset database (jika perlu clean state)
cd /home/scola/odoo && make qa-reset && make qa-seed
cd /home/scola/odoo/scola-fe-v2
Run P0 Flow Tests (2 menit)¶
# Run all 11 P0 critical flows
npx playwright test tests/e2e/flow --grep "@p0"
# Expected output:
# ✅ 11 passed (2.0-2.4 minutes)
# - Student submit assignment (v1, v2, v3) ✓
# - Teacher grades submissions ✓
# - Publish grades (UI + backend idempotency) ✓
# - Student sees published grades ✓
# - Assignment type field consistency ✓
Troubleshoot Failures (2 menit)¶
# 1. Check trace files
ls -lth test-results/ | head -5
# 2. Open trace UI
npx playwright show-trace test-results/<failed-test>/trace.zip
# 3. Check specific error context
cat test-results/<failed-test>/error-context.md
Common P0 Failure Patterns¶
| Error | Probable Cause | Fix (30 sec) |
|---|---|---|
assignment_type_id is undefined |
Field spec mismatch | Verify course.service.js uses assignment_type: { fields: { display_name: {} } } |
Version stays at 2 |
Assignment item not resolved | Check lms.submission creation ensures assignmentItemId exists before version increment |
Publish average = 0 |
Grade computation wrong | Verify gradebook.service.js computes from assignments object, not componentType |
Idempotency key mismatch |
FE/BE key format different | Ensure format: lms-publish:<courseOfferingId>:<studentId>:TUGAS |
Submission modal not showing |
Component not integrated | Check AssignmentDetail.vue has <AssignmentDetailModal :force-edit="true"> |
Evidence Report Template¶
Jika P0 gagal, kumpulkan data ini sebelum escalate:
# 1. Screenshot state (auto-generated in test-results/)
# 2. Backend state
curl -X POST http://127.0.0.1:8069/web/dataset/call_kw/lms.submission/search_read \
-H "Content-Type: application/json" \
-H "Cookie: $(cat .auth/student.json | jq -r '.cookies[0].value')" \
-d '{
"jsonrpc": "2.0",
"params": {
"model": "lms.submission",
"method": "search_read",
"args": [],
"kwargs": {
"domain": [["student_id.user_id", "=", 123]],
"fields": ["assignment_item_id", "version", "state"]
}
}
}' | jq
# 3. Frontend localStorage
# (Open browser DevTools > Application > Local Storage > http://localhost:5173)
# 4. Network logs
# (Playwright trace.zip > Network tab > filter by 'call_kw')
Waktu Total P0 Validation: 2-5 menit (setup + run + basic troubleshoot)
🔬 Evidence-Driven Debugging: "Lihat Dulu, Baru Tebak"¶
Philosophy: Jangan assume anything. Semua harus verified dengan evidence.
Debugging Workflow (15 menit)¶
# STEP 1: Capture UI State (2 menit)
npx playwright test <failing-test> --headed --debug
# - Pause at failure point
# - F12 > Elements > Screenshot relevant DOM
# - F12 > Network > Save HAR file
# - F12 > Console > Save error logs
# STEP 2: Inspect Actual Code (5 menit)
# Jangan percaya dokumentasi/memory. Cek code aktual!
# 2a. Routing inspection
grep -r "path.*assignment" scola-fe-v2/src/router/
# 2b. Component selector inspection
grep -r "data-testid.*submit" scola-fe-v2/src/components/
grep -r "class.*btn.*submit" scola-fe-v2/src/views/
# 2c. API service inspection
grep -r "assignment_type" scola-fe-v2/src/services/
# 2d. Backend field inspection (via web_read)
# (Lihat Network tab di browser > filter 'web_read' > check actual response fields)
# STEP 3: Reproduce Manually (3 menit)
xdg-open http://localhost:5173/login
# - Login sebagai student1/student123
# - Navigate ke failing route
# - Attempt failing action
# - Observe browser console + network tab
# STEP 4: API Validation (2 menit)
curl -X POST http://127.0.0.1:8069/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"student1","password":"student123"}' | jq
curl -X POST http://127.0.0.1:8069/web/dataset/call_kw/lms.course.offering/search_read \
-H "Content-Type: application/json" \
-H "Cookie: session_id=<from-previous-call>" \
-d '{
"jsonrpc": "2.0",
"params": {
"model": "lms.course.offering",
"method": "search_read",
"args": [],
"kwargs": {
"fields": ["name", "assignment_type", "assignment_type_id"]
}
}
}' | jq
# STEP 5: Root Cause Analysis (3 menit)
# Compare:
# - Expected field: <what test assumes>
# - Actual field: <what API returns>
# - Service code: <what FE requests>
# - Component code: <what UI renders>
Evidence Checklist¶
Sebelum claim "ini bug", verify:
- ☐ URL actual match expected? (browser address bar vs router.js)
- ☐ Selector exists in DOM? (DevTools Elements tab)
- ☐ API field name match? (Network tab response vs service code)
- ☐ Backend data exists? (curl to Odoo API)
- ☐ Frontend localStorage correct? (Application tab)
- ☐ Auth session valid? (check .auth/*.json cookies)
- ☐ Environment services running? (ps aux grep)
Jika semua ☑️ tapi masih fail → BUG CONFIRMED → Report dengan evidence
🐛 Known Gotchas & Battle-Tested Solutions¶
Gotcha #1: Assignment Type Field Naming Hell¶
Symptom:
// Error: Cannot read property 'display_name' of undefined
assignment.assignment_type_id?.display_name
Root Cause:
Odoo lms.course.offering model menggunakan assignment_type (Many2one field), bukan assignment_type_id.
Evidence:
# Backend response (actual):
{
"assignment_type": [12, "Tugas"], // ← CORRECT
"assignment_type_id": undefined // ← WRONG!
}
Solution:
// ❌ WRONG (causes undefined)
const fields = {
assignment_type_id: {}
}
// ✅ CORRECT (matches Odoo field name)
const fields = {
assignment_type: {
fields: { display_name: {} }
}
}
Files to Fix:
- src/services/lms/course.service.js
- src/services/lms/assignment.service.js
Verification:
grep -r "assignment_type_id" scola-fe-v2/src/services/
# Should return NO matches!
grep -r "assignment_type:" scola-fe-v2/src/services/
# Should return matches with nested fields
Gotcha #2: Submission Version Not Incrementing¶
Symptom:
Expected version: 3
Actual version: 2 (stuck!)
Root Cause:
lms.submission version hanya increment jika assignment_item_id sudah resolved sebelum create.
Evidence:
// Backend logic (Odoo model):
if (submission.assignment_item_id and submission.state == 'submitted'):
latest_version = search([('assignment_item_id', '=', submission.assignment_item_id)])
submission.version = len(latest_version) + 1
Solution:
// ❌ WRONG (assignment_item_id might not exist yet)
const submission = await createSubmission({ file, grade_component_id })
// version stays at 1!
// ✅ CORRECT (ensure item exists first)
const assignmentItem = await ensureAssignmentItemExists(grade_component_id)
const submission = await createSubmission({
file,
assignment_item_id: assignmentItem.id // ← explicit resolution
})
// version increments correctly: 1 → 2 → 3
Files to Fix:
- src/services/lms/assignment.service.js (submission creation)
- tests/e2e/flow/lms_student_teacher_student_flow.spec.ts (helper functions)
Verification:
# Check submission versions in DB
curl -X POST http://127.0.0.1:8069/web/dataset/call_kw/lms.submission/search_read \
-d '{"params": {"kwargs": {"fields": ["version", "assignment_item_id"]}}}' | jq
Gotcha #3: Publish Grade Average Always Zero¶
Symptom:
// Published grade shows 0 instead of actual average
{ component_type: 'TUGAS', score: 0.0 }
Root Cause:
FE service tried to compute from non-existent componentType instead of assignments object.
Evidence:
// ❌ WRONG CODE (before fix)
const average = gradebook.componentType?.TUGAS?.average || 0
// componentType doesn't exist in API response!
// Actual API structure:
{
assignments: {
'Assignment 1': { score: 85, weight: 0.5 },
'Assignment 2': { score: 90, weight: 0.5 }
}
}
Solution:
// ✅ CORRECT (compute from assignments object)
const assignments = gradebook.assignments || {}
const scores = Object.values(assignments).map(a => a.score || 0)
const average = scores.length > 0
? scores.reduce((sum, s) => sum + s, 0) / scores.length
: 0
const publishData = {
component_type: 'TUGAS',
score: average // ← correct computed value
}
Files to Fix:
- src/services/lms/gradebook.service.js
Verification:
# Check published grades in backend
curl -X POST http://127.0.0.1:8069/web/dataset/call_kw/grade.event/search_read \
-d '{"params": {"kwargs": {"fields": ["component_type", "score"]}}}' | jq
# Score should match computed average, not 0
Gotcha #4: Idempotency Key Format Mismatch¶
Symptom:
// Multiple grade.event records created for same publish action
grade.event: [
{ id: 1, score: 85 },
{ id: 2, score: 85 } // ← DUPLICATE!
]
Root Cause:
FE service uses different key format than backend expects.
Evidence:
// ❌ FE sends (before fix):
idempotencyKey: `publish-${courseOfferingId}-${studentId}-TUGAS`
// ✅ Backend expects:
idempotencyKey: `lms-publish:${courseOfferingId}:${studentId}:TUGAS`
// ^^^^^^^^^ prefix ^^^ colons not dashes
Solution:
// Standardize to backend format
const idempotencyKey = `lms-publish:${courseOfferingId}:${studentId}:TUGAS`
await publishGrades({
student_id: studentId,
course_offering_id: courseOfferingId,
grades: [{ component_type: 'TUGAS', score: average }],
idempotency_key: idempotencyKey
})
Files to Fix:
- src/services/lms/gradebook.service.js
Verification:
# Test idempotency (should only create 1 event)
for i in {1..3}; do
curl -X POST http://127.0.0.1:8069/api/lms/publish-grades \
-d '{"idempotency_key": "lms-publish:1:123:TUGAS"}' | jq
done
# Check event count (should be 1, not 3)
curl -X POST http://127.0.0.1:8069/web/dataset/call_kw/grade.event/search_count \
-d '{"params": {"kwargs": {"domain": [["student_id", "=", 123]]}}}' | jq
Gotcha #5: Submission Modal Not Visible on Detail Page¶
Symptom:
Student navigates to /assignment/:id
Expected: See submission form
Actual: Only see assignment description (no submit button)
Root Cause:
AssignmentDetailModal component not integrated on detail view.
Evidence:
# Check detail view component
cat src/views/AssignmentManagement/Student/AssignmentDetail.vue | grep -i modal
# Returns: (empty) ← component not imported/used!
Solution:
<!-- src/views/AssignmentManagement/Student/AssignmentDetail.vue -->
<template>
<div class="assignment-detail">
<!-- ...existing description... -->
<!-- Add submission modal -->
<AssignmentDetailModal
v-if="assignment"
:assignment="assignment"
:force-edit="true"
@submitted="handleSubmitted"
/>
</div>
</template>
<script setup>
import AssignmentDetailModal from '@/components/Assignment/AssignmentDetailModal.vue'
// Handle submission success
const handleSubmitted = async () => {
// Refresh assignment data to show new version
await fetchAssignmentDetail()
}
</script>
Files to Fix:
- src/views/AssignmentManagement/Student/AssignmentDetail.vue
- src/components/Assignment/AssignmentDetailModal.vue (add forceEdit prop)
Verification:
# Check component integration
grep -A 5 "AssignmentDetailModal" src/views/AssignmentManagement/Student/AssignmentDetail.vue
# Should show import + template usage + force-edit prop
Gotcha #6: File Upload Rejects .txt Test Fixtures¶
Symptom:
<input type="file" accept=".pdf,.doc,.docx">
<!-- Test tries to upload test-submission.txt → REJECTED! -->
Root Cause:
Accept list too restrictive for testing purposes.
Solution:
<!-- Add .txt to accept list for test fixtures -->
<input
type="file"
accept=".pdf,.doc,.docx,.ppt,.pptx,.jpg,.jpeg,.png,.zip,.rar,.txt"
@change="handleFileChange"
>
Files to Fix:
- src/components/Assignment/AssignmentDetailModal.vue
Verification:
# Check accept attribute
grep -r 'accept=".*txt' scola-fe-v2/src/components/
# Should return match in AssignmentDetailModal.vue
Gotcha #7: Session Auth vs UI Login Flakiness¶
Symptom:
Test fails randomly with:
- "Login button not found"
- "Navigation timeout"
- "CORS error"
Root Cause:
UI login depends on:
- Selector stability (UI changes break tests)
- Network timing (CORS/proxy delays)
- Frontend state (localStorage race conditions)
Solution: Use session-based auth instead of UI login:
// ❌ FRAGILE (UI login)
test('submit assignment', async ({ page }) => {
await page.goto('/login')
await page.fill('[data-testid="username"]', 'student1')
await page.fill('[data-testid="password"]', 'student123')
await page.click('[data-testid="submit"]')
// ↑ Fails if selector changes, CORS delays, etc.
})
// ✅ ROBUST (session auth)
test('submit assignment', async ({ page }) => {
// Auth session already loaded from .auth/student.json
// via playwright.config.ts project setup
await page.goto('/assignment/1')
// ↑ Already authenticated, no login needed!
})
Setup in playwright.config.ts:
export default {
projects: [
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
{
name: 'student-tests',
use: {
storageState: '.auth/student.json', // ← pre-authenticated session
},
dependencies: ['setup'],
},
],
}
Setup script (tests/e2e/setup-auth.spec.ts):
import { test as setup } from '@playwright/test'
setup('authenticate as student', async ({ page }) => {
// Login via Odoo API (backend langsung, no UI)
const response = await page.request.post('http://127.0.0.1:8069/api/auth/login', {
data: { username: 'student1', password: 'student123' }
})
const { session_id } = await response.json()
// Save cookies to storage state
await page.context().storageState({ path: '.auth/student.json' })
})
Verification:
# Check session files exist
ls -lh .auth/
# Should show: student.json, teacher.json, parent.json
# Validate session
cat .auth/student.json | jq '.cookies[] | select(.name == "session_id")'
🎯 Quick Decision Tree: "Should I Write This Test?"¶
START
↓
Is this a P0 critical path?
(student submit → teacher grade → publish → student sees grade)
├─ YES → Write test in tests/e2e/flow/ with @p0 tag
└─ NO
↓
Is this a business-critical workflow?
(enroll, unenroll, attendance, grade export)
├─ YES → Write test in tests/e2e/critical/
└─ NO
↓
Is this a sanity check?
(login, navigation, 404 handling)
├─ YES → Write test in tests/e2e/smoke/
└─ NO
↓
Is this a unit/integration test?
(component props, composable logic)
├─ YES → Write test in tests/unit/ or tests/integration/
└─ NO → SKIP (not worth automation cost)
Test Prioritization: 1. P0 Critical Flows (11 tests, 2-5 min runtime) → Block deployment if fail 2. Business Critical (15 tests, 5-10 min runtime) → Warn on fail 3. Smoke Tests (5 tests, 1-2 min runtime) → Fast sanity check 4. Unit/Integration (200+ tests, 30 sec runtime) → Pre-commit validation
📞 Escalation Matrix¶
| Failure Type | Evidence Required | Escalate To | SLA |
|---|---|---|---|
| P0 test fails | trace.zip + error-context.md | Frontend Lead | 2 hours |
| Backend API error | curl output + network HAR | Backend Lead | 4 hours |
| Environment issue | ps aux output + logs | DevOps | 1 hour |
| Test flakiness | 3 consecutive runs with diff results | QA Lead | 1 day |
| Documentation gap | Specific unclear section | Tech Writer | 3 days |
Generated: 2026-01-26
Based on: 39 tests implemented (27 Phase-1 + 12 Phase-2.1)
Troubleshooting time saved: ~4 hours per test suite
Confidence level: Production-ready ✅