Lewati ke isi

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

  1. Asumsi URL routing tanpa verifikasi
  2. ❌ SALAH: Mengasumsikan /teacher/home tanpa cek kode
  3. ✅ BENAR: Cek router.js atau test manual di browser terlebih dahulu

  4. Asumsi selector tanpa inspeksi DOM

  5. ❌ SALAH: Mengasumsikan data-testid="submit-button" ada
  6. ✅ BENAR: Inspect element di browser dev tools, lihat attribute actual

  7. Asumsi login flow tanpa test API

  8. ❌ SALAH: Langsung buat test Playwright dengan asumsi flow
  9. ✅ BENAR: Test login via curl dulu, pahami response structure

  10. Tidak mengecek prerequisite sebelum run

  11. ❌ SALAH: Langsung npx playwright test tanpa cek backend/frontend
  12. ✅ BENAR: Cek ps aux | grep odoo-bin dan ps 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

✅ 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 ✅