Files
halobestie-clone/control_center/tests/e2e/settings.spec.js
ramadhan sjamsani d09e50af55 Phase 3.7: paid pairing flow + returning chat + extension flip
- Backend: payment_sessions + pairing_failures tables; payment.service.js
  and pairing-failure.service.js (new); rewritten pairing.service.js
  (payment-gated blast + targeted "Curhat lagi" + cancel + fallback);
  rewritten extension.service.js (data-driven auto-approve with offline
  safeguard, charge-at-approval); pricing.service.js (extension tiers
  without free trial); mitra-status.service.js (countAvailableMitras
  cached path); 60s sweeper for stale payment sessions
- Backend routes: client.payment.routes, client.mitra-availability.routes,
  internal/failed-pairings.routes; client.chat.routes rewritten for
  payment-gated start + /returning + /cancel + /fallback-to-blast;
  internal/config.routes adds 4 new keys with Valkey invalidate publish
- client_app: mitra-availability poll, payment screen + notifier, pairing
  notifier rewrite (PairingTargetedWaiting + PairingFailed states),
  targeted-waiting overlay + bestie-unavailable dialog, "Curhat lagi"
  CTA, failed-pairing terminal, extension via payment-session
- mitra_app: PairingRequestType enum, returning-chat 20s countdown
  auto-dismiss, extension card "otomatis disetujui" copy
- control_center: 4 new config rows in Settings, Failed Pairings page
  (filter + paginate + action menu), sidebar + route registered
- Test infrastructure: Vitest backend (7/7 pass), Playwright CC (4/4
  pass), Maestro mobile scaffold (CLI install pending)
- Bugs found via Playwright + fixed: LoginPage labels not associated
  with inputs (a11y); backend internal CORS missing PATCH/PUT/DELETE
  in allow-methods (silent settings breakage in browsers since Stage 4)
- Docs: phase3.7.md PRD, phase3.7-plan.md, phase3.7-questions.md (Q&A),
  phase3.7-testing.md (E2E checklist), phase3.7-test-run-2026-05-03.md
  (today's run results)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 23:02:49 +08:00

139 lines
4.9 KiB
JavaScript

import { test, expect } from '@playwright/test'
import { loginAsOperator } from './helpers/auth.js'
import {
getPaymentSessionTimeout,
setPaymentSessionTimeout,
getExtensionDefaultAction,
setExtensionDefaultAction,
} from './helpers/backend-api.js'
/**
* Settings page e2e tests.
*
* These tests exercise two of the four Phase 3.7 config rows end-to-end:
* - payment_session_timeout_minutes (number input)
* - extension_default_action_on_timeout (radio)
*
* Strategy:
* 1. Snapshot the current backend value before each test
* 2. Mutate via the UI
* 3. Reload to confirm persistence (rules out optimistic-only state)
* 4. Restore the original value in afterEach (so the suite is rerunnable)
*/
test.describe('Settings page — Phase 3.7 config rows', () => {
test.beforeEach(async ({ page }) => {
await loginAsOperator(page)
await page.goto('/settings')
await expect(page.getByRole('heading', { name: 'Settings', level: 1 })).toBeVisible()
})
// -------------------------------------------------------------------------
// payment_session_timeout_minutes
// -------------------------------------------------------------------------
test.describe('Batas Waktu Sesi Pembayaran', () => {
let originalValue
test.beforeEach(async () => {
originalValue = await getPaymentSessionTimeout()
})
test.afterEach(async () => {
if (typeof originalValue === 'number') {
await setPaymentSessionTimeout(originalValue)
}
})
test('changing 20 → 25 persists across reload', async ({ page }) => {
await setPaymentSessionTimeout(20)
const section = page
.locator('section', { has: page.getByRole('heading', { name: 'Batas Waktu Sesi Pembayaran' }) })
const input = section.getByRole('spinbutton')
await page.reload()
await expect(input).toHaveValue('20')
// Mutate via the UI. The component fires a mutation on every keystroke
// (no Save button), so .fill() with the final value is enough.
// Wait for the PATCH to complete BEFORE reloading — disabled flip alone
// is racy because fill() returns before React processes the onChange.
const patchResponse = page.waitForResponse(r =>
r.url().includes('/internal/config/payment-session-timeout')
&& r.request().method() === 'PATCH'
&& r.status() === 200
)
await input.fill('25')
await patchResponse
// Reload and confirm the new value sticks.
await page.reload()
await expect(input).toHaveValue('25')
// And confirm the backend agrees.
const persisted = await getPaymentSessionTimeout()
expect(persisted).toBe(25)
})
})
// -------------------------------------------------------------------------
// extension_default_action_on_timeout (radio)
// -------------------------------------------------------------------------
test.describe('Aksi Default Extension', () => {
let originalValue
test.beforeEach(async () => {
originalValue = await getExtensionDefaultAction()
})
test.afterEach(async () => {
if (originalValue) {
await setExtensionDefaultAction(originalValue)
}
})
test('flipping auto-approve <-> auto-reject persists across reload', async ({ page }) => {
// Start from a known state.
await setExtensionDefaultAction('auto_approve')
const section = page
.locator('section', { has: page.getByRole('heading', { name: 'Aksi Default jika Bestie Tidak Menjawab Extension' }) })
const approveRadio = section.getByRole('radio', { name: /Otomatis disetujui/ })
const rejectRadio = section.getByRole('radio', { name: /Otomatis ditolak/ })
await page.reload()
await expect(approveRadio).toBeChecked()
await expect(rejectRadio).not.toBeChecked()
// Flip to auto-reject. Use click() instead of check() — check() races against
// the React re-render that flips `checked` after the mutation roundtrip.
// Wait for the PATCH to complete before asserting persistence.
const patchToReject = page.waitForResponse(r =>
r.url().includes('/internal/config/extension-default-action')
&& r.request().method() === 'PATCH'
&& r.status() === 200
)
await rejectRadio.click()
await patchToReject
await page.reload()
await expect(rejectRadio).toBeChecked()
await expect(approveRadio).not.toBeChecked()
expect(await getExtensionDefaultAction()).toBe('auto_reject')
// Flip back to auto-approve.
const patchToApprove = page.waitForResponse(r =>
r.url().includes('/internal/config/extension-default-action')
&& r.request().method() === 'PATCH'
&& r.status() === 200
)
await approveRadio.click()
await patchToApprove
await page.reload()
await expect(approveRadio).toBeChecked()
expect(await getExtensionDefaultAction()).toBe('auto_approve')
})
})
})