// Verifies the owner/agent family delegation flow end-to-end against the live // Supabase-backed app: owner signs up, creates a family, invites an agent; agent // signs up with that email, accepts the invite, sees the family in their // dashboard, can add an asset — but cannot fire the death trigger (owner-only, // enforced by RLS, not just hidden in the UI). const { chromium } = require('playwright'); const BASE = 'https://moslem04.falahos.my/'; const results = []; function record(name, pass, detail = '') { results.push({ name, pass, detail }); console.log(`${pass ? 'PASS' : 'FAIL'} ${name}${detail ? ' — ' + detail : ''}`); } const stamp = process.argv[2] || String(Math.floor(Math.random() * 1e9)); const ownerEmail = 'nurfalah.e2etest.owner@gmail.com'; const agentEmail = 'nurfalah.e2etest.agent@gmail.com'; const password = 'TestPassword123!'; const familyName = `Test Family ${stamp}`; async function signUp(page, email, name) { await page.goto(BASE, { waitUntil: 'networkidle' }); await page.locator('.mode-btn', { hasText: 'Create account' }).click(); await page.waitForTimeout(200); await page.locator('.field:has-text("Full name") input').fill(name); await page.locator('.field:has-text("Email") input').fill(email); await page.locator('.field:has-text("Password") input').fill(password); await page.locator('button.btn-primary', { hasText: 'Create account' }).click(); await page.waitForTimeout(1500); } async function signIn(page, email) { await page.goto(BASE, { waitUntil: 'networkidle' }); await page.locator('.field:has-text("Email") input').fill(email); await page.locator('.field:has-text("Password") input').fill(password); await page.locator('button.btn-primary', { hasText: 'Sign in' }).click(); await page.waitForTimeout(1500); } async function main() { const mode = process.argv[3]; if (mode === 'signup') { const browser = await chromium.launch(); const ownerPage = await (await browser.newContext()).newPage(); await signUp(ownerPage, ownerEmail, 'Owner Test'); const agentPage = await (await browser.newContext()).newPage(); await signUp(agentPage, agentEmail, 'Agent Test'); await browser.close(); console.log(JSON.stringify({ ownerEmail, agentEmail, password, familyName })); return; } // mode === 'flow' — accounts already confirmed via SQL const browser = await chromium.launch(); // ── Owner: sign in, create family, invite agent ── const ownerCtx = await browser.newContext({ viewport: { width: 390, height: 844 } }); const ownerPage = await ownerCtx.newPage(); await signIn(ownerPage, ownerEmail); const onSwitcher = await ownerPage.locator('.switcher-screen').isVisible().catch(() => false); record('Owner: signs in and lands on family switcher (no family yet)', onSwitcher); await ownerPage.locator('.field:has-text("Family name") input').fill(familyName); await ownerPage.locator('button.btn-primary', { hasText: 'Create family' }).click(); await ownerPage.waitForTimeout(1000); const onMainApp = await ownerPage.locator('nav button.tab', { hasText: 'Coverage' }).isVisible().catch(() => false); record('Owner: creating a family lands on the main app', onMainApp); await ownerPage.locator('nav button[aria-label="Family"]').click(); await ownerPage.waitForTimeout(300); await ownerPage.locator('.field:has-text("Invite an estate agent") input').fill(agentEmail); await ownerPage.locator('button.btn-primary', { hasText: 'Send invite' }).click(); await ownerPage.waitForTimeout(800); const memberRowVisible = await ownerPage.locator('.member-row', { hasText: agentEmail }).isVisible(); record('Owner: inviting agent creates a pending member row', memberRowVisible); // ── Agent: sign in, accept invite, see family, add an asset ── const agentCtx = await browser.newContext({ viewport: { width: 390, height: 844 } }); const agentPage = await agentCtx.newPage(); await signIn(agentPage, agentEmail); const inviteRow = agentPage.locator('.invite-row', { hasText: familyName }); const inviteVisible = await inviteRow.waitFor({ state: 'visible', timeout: 10000 }).then(() => true).catch(() => false); record('Agent: sees pending invite from owner on sign-in', inviteVisible); await inviteRow.locator('.btn-small', { hasText: 'Accept' }).click(); const agentOnMainApp = await agentPage.locator('nav button[aria-label="Coverage"]').waitFor({ state: 'visible', timeout: 10000 }).then(() => true).catch(() => false); record('Agent: accepting invite lands on the main app for that family', agentOnMainApp); await agentPage.locator('nav button[aria-label="Assets"]').click(); await agentPage.waitForTimeout(400); await agentPage.locator('.field:has-text("Description") input').fill('Agent-added asset'); await agentPage.locator('.field:has-text("Estimated value") input').fill('50000'); await agentPage.locator('button.btn-primary', { hasText: 'Add asset' }).click(); const assetAddedByAgent = await agentPage.locator('.asset-row', { hasText: 'Agent-added asset' }).waitFor({ state: 'visible', timeout: 10000 }).then(() => true).catch(() => false); record('Agent: can add an asset to the family estate', assetAddedByAgent); // Owner should see the agent-added asset too (shared, live data — not per-device) await ownerPage.locator('nav button[aria-label="Assets"]').click(); const ownerSeesAgentAsset = await ownerPage.locator('.asset-row', { hasText: 'Agent-added asset' }).waitFor({ state: 'visible', timeout: 10000 }).then(() => true).catch(() => false); record('Owner: sees the asset the agent just added (shared family data)', ownerSeesAgentAsset); // ── Agent tries the Death Trigger — should be visibly restricted ── await agentPage.locator('nav button[aria-label="Trigger"]').click(); const agentRestrictionVisible = await agentPage.locator('.agent-restriction').waitFor({ state: 'visible', timeout: 10000 }).then(() => true).catch(() => false); record('Agent: sees explicit "owner-only to fire" restriction notice', agentRestrictionVisible); // Fill everything needed and confirm the fire button itself won't work for the agent const attestorInputs = agentPage.locator('.attestor-row input'); await attestorInputs.nth(0).fill('Attestor A'); await agentPage.locator('.attestor-row .confirm-btn').nth(0).click(); await agentPage.waitForTimeout(500); await attestorInputs.nth(1).fill('Attestor B'); await agentPage.locator('.attestor-row .confirm-btn').nth(1).click(); await agentPage.waitForTimeout(500); await agentPage.locator('.field:has-text("Date of death") input').fill('2026-08-13'); await agentPage.locator('.field:has-text("Death certificate reference") input').fill('DC-TEST-999'); await agentPage.waitForTimeout(500); const fireBtnDisabledForAgent = await agentPage.locator('button.btn-danger-solid').isDisabled(); record('Agent: fire-trigger button stays disabled even with all fields filled (role check)', fireBtnDisabledForAgent); // ── Owner fires it — should work, enforced by RLS as role=owner ── await ownerPage.locator('nav button[aria-label="Trigger"]').click(); await ownerPage.waitForTimeout(1000); const ownerAttestorInputs = ownerPage.locator('.attestor-row input'); const attestorCount = await ownerAttestorInputs.count(); record('Owner: sees the same attestor data the agent entered (shared)', attestorCount >= 2 && (await ownerAttestorInputs.nth(0).inputValue()) === 'Attestor A'); await ownerPage.waitForFunction(() => { const btn = document.querySelector('button.btn-danger-solid'); return btn && !btn.disabled; }, { timeout: 10000 }).catch(() => {}); const ownerFireBtnEnabled = await ownerPage.locator('button.btn-danger-solid').isEnabled(); record('Owner: fire-trigger button is enabled for the owner role', ownerFireBtnEnabled); if (ownerFireBtnEnabled) { await ownerPage.locator('button.btn-danger-solid').click(); const triggeredBannerVisible = await ownerPage.locator('.triggered-banner').waitFor({ state: 'visible', timeout: 10000 }).then(() => true).catch(() => false); record('Owner: successfully fires the death trigger', triggeredBannerVisible); } await browser.close(); const passCount = results.filter(r => r.pass).length; const failCount = results.length - passCount; console.log(`\n${passCount} passed, ${failCount} failed, ${results.length} total`); if (failCount > 0) results.filter(r => !r.pass).forEach(r => console.log(` - ${r.name}: ${r.detail}`)); process.exit(failCount > 0 ? 1 : 0); } main().catch(e => { console.error('SCRIPT ERROR:', e); process.exit(2); });