- 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>
97 lines
4.1 KiB
JavaScript
97 lines
4.1 KiB
JavaScript
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)
|
|
}
|
|
})
|
|
})
|