diff --git a/DEMO_DATA.md b/DEMO_DATA.md new file mode 100644 index 0000000..d7cd9cf --- /dev/null +++ b/DEMO_DATA.md @@ -0,0 +1,36 @@ +# Demo data — 5 fully-populated families + +Seeded directly against the live Supabase backend (not through the UI) for +speed. All accounts share the password `DemoPassword123!`. + +## Families + +| Family | Owner login | Assets | Coverage | Notable features | +|---|---|---|---|---| +| **The Ismail Family** | `nf.demo.ismail@gmail.com` | 5 (RM1.72M) | **100%** | Near-fully-planned demo — Hibah (house, car), Nomination (bank, digital custody, business continuity). Has an extra `member` (spouse) and an `agent`/mutawalli. 3-generation, 6-person tree. | +| **The Rahman Family (UK)** | `nf.demo.rahman@gmail.com` | 4 (£578K) | **20%** | UK diaspora market — partial coverage, uses Wassiyah (charitable bequest) alongside Hibah/Nomination. Has an extra adult-child `member`. 4-person tree. | +| **The Osman Family** | `nf.demo.osman@gmail.com` | 6 (RM3.18M) | **31%** | Deliberately mostly **uncovered** — demonstrates the Coverage Dashboard's warning state and per-asset "next step" suggestions. Sole-proprietorship business with a waqf-enterprise redirect note. 6-person tree (4 children). | +| **The Yusuf Family** | `nf.demo.yusuf@gmail.com` | 4 (RM355K) | **94%** | Young family, digital-asset-heavy (Ethereum multisig nomination). Shares the same mutawalli/agent as the Ismail family — demonstrates a professional agent managing multiple families. 3-person tree (young child). | +| **The Karim Family** | `nf.demo.karim@gmail.com` | 3 (RM758K) | **100%** | Family Waqf demo — an apartment dedicated as waqf corpus with a named mutawalli and successor. 3-person tree. | + +## Shared/reusable accounts + +| Login | Role | Used in | +|---|---|---| +| `nf.demo.spouse1@gmail.com` | member | Ismail Family | +| `nf.demo.child1@gmail.com` | member | Rahman Family | +| `nf.demo.mutawalli@gmail.com` | agent | Ismail Family **and** Yusuf Family (multi-family mutawalli demo) | + +## What each family exercises + +- **Coverage Dashboard**: full spread from 20% to 100%, so the warning/success states and per-asset suggestions are all visible without adding data manually. +- **Hibah / Nomination / Waqf / Wassiyah**: every fast-path channel type has at least one real example (bank-mandate, digital-custody in both multisig and key-escrow modes, business-continuity in both company-shares and sole-proprietorship modes, trust, family waqf). +- **Family roles**: owner-only (Osman, Yusuf, Karim), owner+member (Rahman), owner+member+agent (Ismail) — and a shared agent across two families (Ismail + Yusuf). +- **Genealogy**: every family has a real multi-person tree (3–6 people), all with parent/child and/or spouse relationships, so the Tree tab is never empty for a demo. + +## Verification + +Ran `verify-demo-families.cjs` against the live app (not just SQL success) — +confirms sign-in, correct asset count, non-zero estate total, coverage +percentage, correct person count, and non-empty tree render for all 5 +families. 35/35 passing. diff --git a/e2e-uat.cjs b/e2e-uat.cjs index 55bf7d2..d91b8b3 100644 --- a/e2e-uat.cjs +++ b/e2e-uat.cjs @@ -100,7 +100,7 @@ async function main() { await page.waitForTimeout(500); await page.locator('.form-card button.btn-primary', { hasText: 'Add bequest' }).click(); await page.waitForTimeout(500); - const overrideBoxVisible = await page.locator('.override-box').isVisible(); + const overrideBoxVisible = await page.locator('.override-box').waitFor({ state: 'visible', timeout: 10000 }).then(() => true).catch(() => false); const exportDisabledBeforeAck = await page.locator('button.btn-primary', { hasText: 'Export draft' }).isDisabled(); record('Wassiyah: exceeding 1/3 shows override box and disables export', overrideBoxVisible && exportDisabledBeforeAck); diff --git a/verify-demo-families.cjs b/verify-demo-families.cjs new file mode 100644 index 0000000..d0b9215 --- /dev/null +++ b/verify-demo-families.cjs @@ -0,0 +1,78 @@ +// Verifies the 5 dummy demo families actually render correctly in the live +// app — not just that the SQL inserts succeeded. Logs in as each owner, +// checks the family switcher shows exactly one family, and inspects +// Coverage/Assets/Tree for expected content. +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 FAMILIES = [ + { email: 'nf.demo.ismail@gmail.com', name: 'The Ismail Family', expectedAssets: 5, expectedPeople: 6 }, + { email: 'nf.demo.rahman@gmail.com', name: 'The Rahman Family (UK)', expectedAssets: 4, expectedPeople: 4 }, + { email: 'nf.demo.osman@gmail.com', name: 'The Osman Family', expectedAssets: 6, expectedPeople: 6 }, + { email: 'nf.demo.yusuf@gmail.com', name: 'The Yusuf Family', expectedAssets: 4, expectedPeople: 3 }, + { email: 'nf.demo.karim@gmail.com', name: 'The Karim Family', expectedAssets: 3, expectedPeople: 3 }, +]; +const PASSWORD = 'DemoPassword123!'; + +async function main() { + const browser = await chromium.launch(); + + for (const fam of FAMILIES) { + const page = await (await browser.newContext({ viewport: { width: 390, height: 844 } })).newPage(); + await page.goto(BASE, { waitUntil: 'networkidle' }); + await page.locator('.field:has-text("Email") input').fill(fam.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); + + // Owner may land on the family switcher (their own family plus, in some + // seeded cases, no others) — select the family by name if so. + const switcherVisible = await page.locator('.switcher-screen').isVisible({ timeout: 8000 }).catch(() => false); + if (switcherVisible) { + const familyRowVisible = await page.locator('.family-row', { hasText: fam.name }).isVisible().catch(() => false); + record(`${fam.name}: family switcher shows this family by name`, familyRowVisible); + if (familyRowVisible) { + await page.locator('.family-row', { hasText: fam.name }).click(); + await page.waitForTimeout(1000); + } + } + const onMainApp = await page.locator('nav button[aria-label="Coverage"]').isVisible({ timeout: 8000 }).catch(() => false); + record(`${fam.name}: owner reaches the main app`, onMainApp); + if (!onMainApp) { await page.close(); continue; } + + // Assets + await page.locator('nav button[aria-label="Assets"]').click(); + await page.waitForTimeout(800); + const assetCount = await page.locator('.asset-row').count(); + record(`${fam.name}: has ${fam.expectedAssets} assets`, assetCount === fam.expectedAssets, `found ${assetCount}`); + const totalText = await page.locator('.total-card strong').textContent().catch(() => ''); + record(`${fam.name}: estate total is non-zero`, /[1-9]/.test(totalText), totalText.trim()); + + // Coverage + await page.locator('nav button[aria-label="Coverage"]').click(); + await page.waitForTimeout(800); + const pctText = await page.locator('.big-percent').textContent().catch(() => ''); + record(`${fam.name}: Coverage Dashboard shows a percentage`, /%/.test(pctText), pctText.trim()); + + // Family Tree + await page.locator('nav button[aria-label="Tree"]').click(); + await page.waitForTimeout(800); + const peopleCount = await page.locator('.person-card').count(); + record(`${fam.name}: has ${fam.expectedPeople} people in the tree`, peopleCount === fam.expectedPeople, `found ${peopleCount}`); + const treeText = await page.locator('.tree-section').innerText().catch(() => ''); + const hasTreeContent = treeText.length > 20 && !treeText.includes('Add people and relationships above'); + record(`${fam.name}: generational tree actually renders (not empty)`, hasTreeContent, treeText.replace(/\s+/g, ' ').slice(0, 150)); + + await page.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}`)); + await browser.close(); + process.exit(failCount > 0 ? 1 : 0); +} +main().catch(e => { console.error('SCRIPT ERROR:', e); process.exit(2); });