From 8c75baf30a17c5592faff0d12b2a6ba993672e95 Mon Sep 17 00:00:00 2001 From: ismail Date: Sun, 14 Jun 2026 19:05:07 +0300 Subject: [PATCH] test: make inquiry/observation Flow A visible (UI-driven with API fallback) Flow A now drives the real screens - detail page, Send-to modal, dept-response page, Accept button - so the run is watchable in headed mode. Each visible step falls back to a direct POST if the UI doesn't persist, so the flow still completes reliably. Flows B/C unchanged. --- .../inquiry-observation-dept-workflow.spec.ts | 78 ++++++++++++++++--- 1 file changed, 66 insertions(+), 12 deletions(-) diff --git a/e2e/tests/workflows/inquiry-observation-dept-workflow.spec.ts b/e2e/tests/workflows/inquiry-observation-dept-workflow.spec.ts index 579b2d0..abe8479 100644 --- a/e2e/tests/workflows/inquiry-observation-dept-workflow.spec.ts +++ b/e2e/tests/workflows/inquiry-observation-dept-workflow.spec.ts @@ -28,6 +28,7 @@ interface KindCfg { label: string; module: string; seedKind: string; + detail: (id: string) => string; sendTo: (id: string) => string; sendToken: (id: string) => string; deptResponse: (id: string) => string; @@ -44,6 +45,7 @@ interface KindCfg { const KINDS: KindCfg[] = [ { label: 'Inquiry', module: 'InquiryDeptResponse', seedKind: 'inquiry', + detail: (id) => `/inquiries/${id}/`, sendTo: (id) => `/inquiries/${id}/send-to/`, sendToken: (id) => `/inquiries/${id}/transfer-to-department/`, deptResponse: (id) => `/inquiries/${id}/department-response/`, @@ -56,6 +58,7 @@ const KINDS: KindCfg[] = [ }, { label: 'Observation', module: 'ObservationDeptResponse', seedKind: 'observation', + detail: (id) => `/observations/${id}`, sendTo: (id) => `/observations/${id}/send-to/`, sendToken: (id) => `/observations/${id}/send-to-department/`, deptResponse: (id) => `/observations/${id}/department-response/`, @@ -139,21 +142,72 @@ async function flowA(page: Page, K: KindCfg) { const s = seed(K.seedKind); itemId = s.itemId; observe(M, 'A-seed', 'INFO', `${K.label} ${itemId}`, { role: PXT }); - // 1. PX send (AJAX) + + // Helper: try the visible UI path; if it doesn't achieve the expected state, fall back to a direct POST. + const ensureState = async (want: Record, step: string, role: string, fallback?: () => Promise) => { + const before = state(K.seedKind, itemId); + const okBefore = Object.entries(want).every(([k, v]) => (v === 'NOT_NONE' ? (before[k] && before[k] !== 'NONE') : before[k] === v)); + if (!okBefore && fallback) { await fallback(); } + assertSt(M, K.seedKind, itemId, want, step, role); + }; + + // 1. PX send - VISIBLE: detail page + Send-to modal await login(page, PXT, M); await ensureAuth(page, PXT, M); - await postObs(page, M, `${BASE_URL}${K.sendTo(itemId)}`, { recipient_type: 'department', department_id: s.deptId, contact_person_id: s.champStaffId, note: 'E2E send' }, 'A-send', PXT); - assertSt(M, K.seedKind, itemId, { [K.sentField]: K.sentWant }, 'A-send-state', PXT); - // 2. champion responds (logged-in) + await page.goto(`${BASE_URL}${K.detail(itemId)}`); + await page.waitForLoadState('domcontentloaded'); + await page.waitForTimeout(700); + await page.evaluate((args) => { + const fn = (window as any).showSendModal; + if (typeof fn === 'function') { try { fn(args.id, args.kind); } catch { /* ignore */ } } + const m = document.getElementById('sendToModal'); + if (m) m.classList.remove('hidden'); + }, { id: itemId, kind: K.seedKind }); + await page.waitForTimeout(500); + await page.locator('input[name="recipient_type"][value="department"]').first().check({ force: true }).catch(() => {}); + await page.locator('select[name="department_id"]').first().selectOption({ value: s.deptId }).catch(() => {}); + await page.waitForTimeout(1200); + await page.locator('select[name="contact_person_id"]').first().selectOption({ value: s.champStaffId }).catch(() => {}); + await page.waitForTimeout(300); + await page.locator('#sendToModal button[type="submit"], #sendToModal button:not([type="button"])').first().click({ force: true }).catch(() => {}); + await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {}); + await page.waitForTimeout(1200); + await ensureState({ [K.sentField]: K.sentWant }, 'A-send-state', PXT, + async () => { await postObs(page, M, `${BASE_URL}${K.sendTo(itemId)}`, { recipient_type: 'department', department_id: s.deptId, contact_person_id: s.champStaffId, note: 'E2E send' }, 'A-send-fallback', PXT); }); + + // 2. Champion responds - VISIBLE: dept-response page await login(page, CHAMP, M); await ensureAuth(page, CHAMP, M); - await postObs(page, M, `${BASE_URL}${K.deptResponse(itemId)}`, { response_en: `E2E ${K.label} champion response (A)` }, 'A-champion-response', CHAMP); - assertSt(M, K.seedKind, itemId, { department_response_en_set: 'True', dept_response_acceptance_status: 'pending' }, 'A-champion-state', CHAMP); - // 3. PX accepts + await page.goto(`${BASE_URL}${K.deptResponse(itemId)}`); + await page.waitForLoadState('domcontentloaded'); + await page.waitForTimeout(500); + const tb = await bodyHasTraceback(page); + if (!tb) { + const ta = page.locator('textarea[name="response_en"]').first(); + if (await ta.count()) { + await ta.fill(`E2E ${K.label} champion response (A)`); + // submit the response form specifically + await page.locator('form[action*="department-response"] button[type="submit"], textarea[name="response_en"] ~ * button[type="submit"], button[type="submit"]').first().click({ force: true }).catch(() => {}); + await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {}); + await page.waitForTimeout(1000); + } + } + await ensureState({ department_response_en_set: 'True', dept_response_acceptance_status: 'pending' }, 'A-champion-state', CHAMP, + async () => { await postObs(page, M, `${BASE_URL}${K.deptResponse(itemId)}`, { response_en: `E2E ${K.label} champion response (A)` }, 'A-champion-fallback', CHAMP); }); + + // 3. PX accepts - VISIBLE: click Accept on the detail page await login(page, PXT, M); await ensureAuth(page, PXT, M); - await postObs(page, M, `${BASE_URL}${K.review(itemId)}`, { acceptance_status: 'acceptable' }, 'A-px-accept', PXT); - assertSt(M, K.seedKind, itemId, { dept_response_acceptance_status: 'acceptable' }, 'A-accept-state', PXT); - // 4. resolve (inquiry needs a PX response first). Best-effort: some status - // machines (observation) can't jump straight to resolved; the dept-response - // flow itself is already proven by the acceptance assertion above. + await page.goto(`${BASE_URL}${K.detail(itemId)}`); + await page.waitForLoadState('domcontentloaded'); + await page.waitForTimeout(600); + const acceptForm = page.locator('form').filter({ has: page.locator('input[name="acceptance_status"][value="acceptable"]') }).first(); + if (await acceptForm.count()) { + await acceptForm.locator('button[type="submit"]').first().click({ force: true }).catch(() => {}); + await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {}); + await page.waitForTimeout(1000); + } + await ensureState({ dept_response_acceptance_status: 'acceptable' }, 'A-accept-state', PXT, + async () => { await postObs(page, M, `${BASE_URL}${K.review(itemId)}`, { acceptance_status: 'acceptable' }, 'A-accept-fallback', PXT); }); + + // 4. Resolve (best-effort; some status machines can't jump to resolved) if (K.pxRespond) await postObs(page, M, `${BASE_URL}${K.pxRespond(itemId)}`, { response_en: `E2E ${K.label} PX response` }, 'A-px-respond', PXT); await postForm(page, `${BASE_URL}${K.activate(itemId)}`, {}); await postObs(page, M, `${BASE_URL}${K.changeStatus(itemId)}`, { status: 'resolved', note: 'E2E resolved' }, 'A-resolve', PXT);