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>
This commit is contained in:
96
control_center/tests/e2e/failed-pairings.spec.js
Normal file
96
control_center/tests/e2e/failed-pairings.spec.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { loginAsOperator } from './helpers/auth.js'
|
||||
import { listFailedPairings, ensureAtLeastOneFailedPairing } from './helpers/backend-api.js'
|
||||
|
||||
/**
|
||||
* Failed Pairings page e2e tests.
|
||||
*
|
||||
* NOTE: There is currently no public test-seed endpoint for failed_pairings.
|
||||
* Production rows are inserted by the backend pairing service when a real
|
||||
* pairing fails. These tests therefore rely on the database already containing
|
||||
* at least one row (any cause). If the table is empty the tests are skipped
|
||||
* with a clear TODO so the user can either:
|
||||
* - run the real pairing flow once to generate real fixtures, or
|
||||
* - add a backend test-only seed route (see helpers/backend-api.js).
|
||||
*/
|
||||
|
||||
test.describe('Failed Pairings page', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await loginAsOperator(page)
|
||||
})
|
||||
|
||||
test('renders the table when at least one row exists', async ({ page }) => {
|
||||
let seeded
|
||||
try {
|
||||
seeded = await ensureAtLeastOneFailedPairing()
|
||||
} catch (e) {
|
||||
test.skip(true, `No failed-pairings rows in DB. ${e.message}`)
|
||||
return
|
||||
}
|
||||
|
||||
await page.goto('/failed-pairings')
|
||||
await expect(page.getByRole('heading', { name: 'Failed Pairings', level: 1 })).toBeVisible()
|
||||
|
||||
// The table headers should always render once data has loaded.
|
||||
await expect(page.getByRole('columnheader', { name: 'Created' })).toBeVisible()
|
||||
await expect(page.getByRole('columnheader', { name: 'Cause' })).toBeVisible()
|
||||
await expect(page.getByRole('columnheader', { name: 'Operator Action' })).toBeVisible()
|
||||
|
||||
// At least one data row (skip the header row).
|
||||
const dataRows = page.locator('table tbody tr')
|
||||
const count = await dataRows.count()
|
||||
expect(count).toBeGreaterThanOrEqual(1)
|
||||
|
||||
// The "empty" placeholder row should NOT be visible when we have data.
|
||||
await expect(page.getByText('Belum ada data failed pairings.')).not.toBeVisible()
|
||||
|
||||
// Sanity check — total label matches what the backend reports.
|
||||
await expect(page.getByText(`(${seeded.total} total)`)).toBeVisible()
|
||||
})
|
||||
|
||||
test('cause-tag filter narrows the visible rows', async ({ page }) => {
|
||||
// We need rows of at least 2 distinct cause tags to make a meaningful
|
||||
// assertion about filtering. If the DB has fewer, skip with a clear
|
||||
// message rather than producing a fake-positive.
|
||||
const all = await listFailedPairings({ limit: 200 })
|
||||
const distinctCauses = [...new Set((all?.rows ?? []).map((r) => r.cause_tag))]
|
||||
if (distinctCauses.length < 2) {
|
||||
test.skip(
|
||||
true,
|
||||
`Need >=2 distinct cause_tags in failed_pairings to test filter. Found: ${distinctCauses.join(', ') || '(none)'}.`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const targetCause = distinctCauses[0]
|
||||
const expectedFiltered = await listFailedPairings({ causeTags: [targetCause], limit: 200 })
|
||||
|
||||
await page.goto('/failed-pairings')
|
||||
await expect(page.getByRole('heading', { name: 'Failed Pairings', level: 1 })).toBeVisible()
|
||||
|
||||
// Capture pre-filter total — must be >= filtered total or assertion is bogus.
|
||||
const totalBefore = all.total
|
||||
expect(totalBefore).toBeGreaterThan(expectedFiltered.total)
|
||||
|
||||
// Tick the cause checkbox. The filter section labels the checkbox with the
|
||||
// human label from PairingFailureCauseLabel — find it by its associated text.
|
||||
// Map the cause_tag to its label by re-importing the constants.
|
||||
const { PairingFailureCauseLabel } = await import(
|
||||
'../../src/core/constants.js'
|
||||
)
|
||||
const targetLabel = PairingFailureCauseLabel[targetCause]
|
||||
|
||||
await page.getByRole('checkbox', { name: targetLabel }).check()
|
||||
|
||||
// Expect the new total to match the API-computed expectation.
|
||||
await expect(page.getByText(`(${expectedFiltered.total} total)`)).toBeVisible()
|
||||
|
||||
// And every visible row should reflect the chosen cause tag.
|
||||
const causeCells = page.locator('table tbody tr td:nth-child(4)')
|
||||
const visibleCount = await causeCells.count()
|
||||
expect(visibleCount).toBeGreaterThan(0)
|
||||
for (let i = 0; i < visibleCount; i++) {
|
||||
await expect(causeCells.nth(i)).toHaveText(targetLabel)
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user