diff --git a/e2e-auth-helper.cjs b/e2e-auth-helper.cjs new file mode 100644 index 0000000..c925c29 --- /dev/null +++ b/e2e-auth-helper.cjs @@ -0,0 +1,25 @@ +// Shared sign-in prelude for the pre-auth-gate E2E suites. Each suite creates +// its own fresh family per run (unique name) so accumulated data from a +// previous run's assets/hibah/etc. can never bleed into another run's +// percentage/coverage assertions — same isolation the old per-context +// localStorage gave for free, now done explicitly since data is shared and +// persistent in Supabase instead of wiped with the browser context. +const OWNER_EMAIL = 'nurfalah.e2etest.owner@gmail.com'; +const OWNER_PASSWORD = 'TestPassword123!'; + +async function signInFreshFamily(page, BASE, familyLabel) { + await page.goto(BASE, { waitUntil: 'networkidle' }); + await page.locator('.field:has-text("Email") input').fill(OWNER_EMAIL); + await page.locator('.field:has-text("Password") input').fill(OWNER_PASSWORD); + await page.locator('button.btn-primary', { hasText: 'Sign in' }).click(); + await page.waitForTimeout(1500); + + const familyName = `${familyLabel || 'E2E'} ${Date.now()}-${Math.floor(Math.random() * 1e6)}`; + await page.locator('.field:has-text("Family name") input').fill(familyName); + await page.locator('button.btn-primary', { hasText: 'Create family' }).click(); + await page.waitForTimeout(1200); + + return familyName; +} + +module.exports = { signInFreshFamily, OWNER_EMAIL, OWNER_PASSWORD }; diff --git a/e2e-business.cjs b/e2e-business.cjs index 28cfe2d..eacdb6a 100644 --- a/e2e-business.cjs +++ b/e2e-business.cjs @@ -1,6 +1,7 @@ // Verifies business interests get covered via a proper continuity instrument, // including the sole-proprietorship warning and the waqf-enterprise redirect. const { chromium } = require('playwright'); +const { signInFreshFamily } = require('./e2e-auth-helper.cjs'); const BASE = 'https://moslem04.falahos.my/'; const results = []; const consoleErrors = []; @@ -12,10 +13,16 @@ async function main() { page.on('console', m => { if (m.type() === 'error') consoleErrors.push(m.text()); }); page.on('pageerror', e => consoleErrors.push(e.message)); - await page.goto(BASE, { waitUntil: 'networkidle' }); - const clickTab = async label => { await page.locator('nav button.tab', { hasText: label }).click(); await page.waitForTimeout(200); }; + await signInFreshFamily(page, BASE, 'e2e-business'); + const clickTab = async label => { await page.locator('nav button.tab', { hasText: label }).click(); await page.waitForTimeout(500); }; const selectByText = async (locator, text) => { - const val = await locator.evaluate((el, t) => Array.from(el.options).find(o => o.textContent.includes(t))?.value, text); + // Options load async from Supabase now — poll until the matching one shows up. + let val; + for (let i = 0; i < 20; i++) { + val = await locator.evaluate((el, t) => Array.from(el.options).find(o => o.textContent.includes(t))?.value, text); + if (val) break; + await new Promise(r => setTimeout(r, 300)); + } await locator.selectOption(val); }; @@ -25,7 +32,7 @@ async function main() { await page.locator('.field:has-text("Estimated value") input').fill('300000'); await page.locator('.form-card select').first().selectOption('Business interest'); await page.locator('button.btn-primary', { hasText: 'Add asset' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); await clickTab('Coverage'); const rowBefore = await page.locator('.asset-row', { hasText: 'Bakery' }).textContent(); @@ -34,9 +41,9 @@ async function main() { // Go to Nominate, select business-continuity channel await clickTab('Nominate'); await selectByText(page.locator('select').first(), 'Bakery'); - await page.waitForTimeout(200); + await page.waitForTimeout(500); await page.locator('.form-card select').nth(1).selectOption('business-continuity'); - await page.waitForTimeout(200); + await page.waitForTimeout(500); const businessFieldsVisible = await page.locator('.trust-fields').isVisible(); record('Nomination: business continuity fields appear', businessFieldsVisible); @@ -47,25 +54,25 @@ async function main() { // Switch to sole proprietorship -> warning should appear await page.locator('.trust-fields select').first().selectOption('sole-prop'); - await page.waitForTimeout(200); + await page.waitForTimeout(500); const soleWarningVisible = await page.locator('.business-warning').first().isVisible(); const soleWarningText = await page.locator('.business-warning').first().textContent(); record('Nomination: sole-proprietorship shows "dies with you" warning', soleWarningVisible && soleWarningText.includes('dies with you'), soleWarningText.trim().slice(0, 150)); // Switch back to company, pick shareholder buy-sell, fill and save await page.locator('.trust-fields select').first().selectOption('company'); - await page.waitForTimeout(150); + await page.waitForTimeout(500); const instrumentSelect = page.locator('.trust-fields select').nth(1); await instrumentSelect.selectOption('shareholder-buy-sell'); - await page.waitForTimeout(150); + await page.waitForTimeout(500); const inputs = page.locator('.trust-fields .field input'); await inputs.nth(0).fill('Co-founder Rahman'); await inputs.nth(1).fill('Fair market valuation, Takaful-funded'); await page.locator('button.btn-primary', { hasText: 'Add business continuity instrument' }).click(); - await page.waitForTimeout(300); + await page.waitForTimeout(600); - const rowCreated = await page.locator('.nomination-row', { hasText: 'Bakery' }).isVisible(); + const rowCreated = await page.locator('.nomination-row', { hasText: 'Bakery' }).waitFor({ state: 'visible', timeout: 10000 }).then(() => true).catch(() => false); record('Nomination: business continuity row created', rowCreated); const [download] = await Promise.all([ @@ -82,9 +89,9 @@ async function main() { // Waqf-enterprise redirect path: try selecting it and confirm Add button disables await clickTab('Nominate'); await page.locator('.form-card select').nth(1).selectOption('business-continuity'); - await page.waitForTimeout(150); + await page.waitForTimeout(500); await page.locator('.trust-fields select').nth(1).selectOption('waqf-enterprise'); - await page.waitForTimeout(150); + await page.waitForTimeout(500); const redirectNoteVisible = await page.locator('.business-warning', { hasText: 'Family Waqf Designator' }).isVisible(); const addDisabled = await page.locator('button.btn-primary', { hasText: 'business continuity' }).isDisabled(); record('Nomination: waqf-enterprise option redirects to Family Waqf, disables Add', redirectNoteVisible && addDisabled); @@ -98,9 +105,9 @@ async function main() { await page.locator('.attestor-row .confirm-btn').nth(1).click(); await page.locator('.field:has-text("Date of death") input').fill('2026-08-13'); await page.locator('.field:has-text("Death certificate reference") input').fill('DC-BIZ-001'); - await page.waitForTimeout(200); + await page.waitForTimeout(500); await page.locator('button.btn-danger-solid').click(); - await page.waitForTimeout(300); + await page.waitForTimeout(600); const packetForBiz = await page.locator('.packet-row', { hasText: 'Bakery' }).isVisible(); record('Death Trigger: execution packet generated for business via continuity instrument', packetForBiz); diff --git a/e2e-digital-vehicle.cjs b/e2e-digital-vehicle.cjs index 8272b81..38be532 100644 --- a/e2e-digital-vehicle.cjs +++ b/e2e-digital-vehicle.cjs @@ -2,6 +2,7 @@ // mismatched bank-mandate fields), and vehicles get correctly suggested Hibah // (not overkill trust structure) and can actually be covered that way. const { chromium } = require('playwright'); +const { signInFreshFamily } = require('./e2e-auth-helper.cjs'); const BASE = 'https://moslem04.falahos.my/'; const results = []; const consoleErrors = []; @@ -13,10 +14,16 @@ async function main() { page.on('console', m => { if (m.type() === 'error') consoleErrors.push(m.text()); }); page.on('pageerror', e => consoleErrors.push(e.message)); - await page.goto(BASE, { waitUntil: 'networkidle' }); - const clickTab = async label => { await page.locator('nav button.tab', { hasText: label }).click(); await page.waitForTimeout(200); }; + await signInFreshFamily(page, BASE, 'e2e-digital-vehicle'); + const clickTab = async label => { await page.locator('nav button.tab', { hasText: label }).click(); await page.waitForTimeout(500); }; const selectByText = async (locator, text) => { - const val = await locator.evaluate((el, t) => Array.from(el.options).find(o => o.textContent.includes(t))?.value, text); + // Options load async from Supabase now — poll until the matching one shows up. + let val; + for (let i = 0; i < 20; i++) { + val = await locator.evaluate((el, t) => Array.from(el.options).find(o => o.textContent.includes(t))?.value, text); + if (val) break; + await new Promise(r => setTimeout(r, 300)); + } await locator.selectOption(val); }; @@ -26,7 +33,7 @@ async function main() { await page.locator('.field:has-text("Estimated value") input').fill('150000'); await page.locator('.form-card select').first().selectOption('Digital assets'); await page.locator('button.btn-primary', { hasText: 'Add asset' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); await clickTab('Coverage'); const digitalRowBefore = await page.locator('.asset-row', { hasText: 'Bitcoin' }).textContent(); @@ -34,9 +41,9 @@ async function main() { await clickTab('Nominate'); await selectByText(page.locator('select').first(), 'Bitcoin'); - await page.waitForTimeout(200); + await page.waitForTimeout(500); await page.locator('.form-card select').nth(1).selectOption('digital-custody'); - await page.waitForTimeout(200); + await page.waitForTimeout(500); const digitalFieldsVisible = await page.locator('.trust-fields').isVisible(); record('Nomination: digital-custody-specific fields appear', digitalFieldsVisible); @@ -44,12 +51,12 @@ async function main() { // Switch to multisig -> warning appears (only one select in the digital-custody fields: Custody type) const custodySelect = page.locator('.trust-fields select').nth(0); await custodySelect.selectOption('multisig'); - await page.waitForTimeout(150); + await page.waitForTimeout(500); const multisigWarningVisible = await page.locator('.business-warning').isVisible(); record('Nomination: multi-sig custody shows the co-signer-setup warning', multisigWarningVisible); await custodySelect.selectOption('key-escrow'); - await page.waitForTimeout(150); + await page.waitForTimeout(500); const escrowWarningVisible = await page.locator('.business-warning').isVisible(); const escrowWarningText = await page.locator('.business-warning').textContent(); record('Nomination: key-escrow shows "never store the actual key" warning', escrowWarningVisible && escrowWarningText.includes('Never store'), escrowWarningText.trim().slice(0, 150)); @@ -59,7 +66,7 @@ async function main() { await digitalInputs.nth(1).fill('Executor Ahmad'); await digitalInputs.nth(2).fill('Sealed instructions with lawyer, ref #45'); await page.locator('button.btn-primary', { hasText: 'digital custody plan' }).click(); - await page.waitForTimeout(300); + await page.waitForTimeout(600); const digitalRowCreated = await page.locator('.nomination-row', { hasText: 'Bitcoin' }).isVisible(); record('Nomination: digital custody row created', digitalRowCreated); @@ -80,7 +87,7 @@ async function main() { await page.locator('.field:has-text("Estimated value") input').fill('45000'); await page.locator('.form-card select').first().selectOption('Vehicle'); await page.locator('button.btn-primary', { hasText: 'Add asset' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); await clickTab('Coverage'); const vehicleRowBefore = await page.locator('.asset-row', { hasText: 'Toyota' }).textContent(); @@ -94,11 +101,11 @@ async function main() { const linkSelect = page.locator('select').first(); await selectByText(linkSelect, 'Toyota'); await page.locator('button.btn-primary', { hasText: 'Log this hibah' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); await page.locator('.guard-buttons button.btn-secondary', { hasText: 'No' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); await page.locator('button.btn-primary', { hasText: 'Confirm and save' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); await clickTab('Coverage'); const overallPct = await page.locator('.big-percent').textContent(); diff --git a/e2e-fastpath.cjs b/e2e-fastpath.cjs index c47ee60..6471f16 100644 --- a/e2e-fastpath.cjs +++ b/e2e-fastpath.cjs @@ -1,6 +1,7 @@ // Targeted E2E for the new fast-path mechanism: Coverage, Hibah asset-link, // Waqf corpus-link, Nomination, Death Trigger. Run after the general e2e-uat.cjs. const { chromium } = require('playwright'); +const { signInFreshFamily } = require('./e2e-auth-helper.cjs'); const BASE = 'https://moslem04.falahos.my/'; const results = []; const consoleErrors = []; @@ -12,9 +13,9 @@ async function main() { page.on('console', m => { if (m.type() === 'error') consoleErrors.push(m.text()); }); page.on('pageerror', e => consoleErrors.push(e.message)); - await page.goto(BASE, { waitUntil: 'networkidle' }); + await signInFreshFamily(page, BASE, 'e2e-fastpath'); - const clickTab = async label => { await page.locator('nav button.tab', { hasText: label }).click(); await page.waitForTimeout(200); }; + const clickTab = async label => { await page.locator('nav button.tab', { hasText: label }).click(); await page.waitForTimeout(500); }; // New tabs exist for (const label of ['Coverage', 'Nominate', 'Trigger']) { @@ -27,7 +28,7 @@ async function main() { await page.locator('.field:has-text("Description") input').fill('Land parcel, Perak'); await page.locator('.field:has-text("Estimated value") input').fill('400000'); await page.locator('button.btn-primary', { hasText: 'Add asset' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); // Coverage should show 0% before any fast-path instrument await clickTab('Coverage'); @@ -42,14 +43,14 @@ async function main() { await page.locator('.field:has-text("Relation to you") input').fill('nephew'); await page.locator('.field:has-text("Asset / gift description") input').fill('Land parcel gift'); const linkSelect = page.locator('select').first(); + await linkSelect.locator('option').nth(1).waitFor({ state: 'attached', timeout: 10000 }).catch(() => {}); await linkSelect.selectOption({ index: 1 }); await page.locator('button.btn-primary', { hasText: 'Log this hibah' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); await page.locator('.guard-buttons button.btn-secondary', { hasText: 'No' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); await page.locator('button.btn-primary', { hasText: 'Confirm and save' }).click(); - await page.waitForTimeout(200); - const fastBadgeVisible = await page.locator('.fast-badge').isVisible(); + const fastBadgeVisible = await page.locator('.fast-badge').waitFor({ state: 'visible', timeout: 10000 }).then(() => true).catch(() => false); record('Hibah: linked gift shows fast-path badge', fastBadgeVisible); // Coverage should now show 100% @@ -71,18 +72,17 @@ async function main() { await page.locator('.field:has-text("Estimated value") input').fill('200000'); await page.locator('.form-card select').first().selectOption('Cash / Bank'); await page.locator('button.btn-primary', { hasText: 'Add asset' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); await clickTab('Nominate'); - await page.locator('select').first().selectOption({ label: /EPF savings/ }).catch(async () => { - const opts = await page.locator('select').first().locator('option').count(); - await page.locator('select').first().selectOption({ index: opts - 1 }); - }); + const nomSelect = page.locator('select').first(); + await nomSelect.locator('option').nth(1).waitFor({ state: 'attached', timeout: 10000 }).catch(() => {}); + const epfVal = await nomSelect.evaluate(el => Array.from(el.options).find(o => o.textContent.includes('EPF savings'))?.value); + await nomSelect.selectOption(epfVal || { index: 1 }); await page.locator('.field:has-text("Nominee name") input').fill('My Wife'); await page.locator('.field:has-text("Institution") input').fill('KWSP'); await page.locator('button.btn-primary', { hasText: 'Add nomination' }).click(); - await page.waitForTimeout(200); - const nominationRowVisible = await page.locator('.nomination-row', { hasText: 'My Wife' }).isVisible(); + const nominationRowVisible = await page.locator('.nomination-row', { hasText: 'My Wife' }).waitFor({ state: 'visible', timeout: 10000 }).then(() => true).catch(() => false); record('Nomination: adding a nomination creates a row', nominationRowVisible); // Death Trigger: cannot fire without threshold @@ -97,12 +97,12 @@ async function main() { await page.locator('.attestor-row .confirm-btn').nth(1).click(); await page.locator('.field:has-text("Date of death") input').fill('2026-08-13'); await page.locator('.field:has-text("Death certificate reference") input').fill('DC-2026-00123'); - await page.waitForTimeout(200); + await page.waitForTimeout(500); const fireBtnEnabled = await page.locator('button.btn-danger-solid').isEnabled(); record('Death Trigger: fire button enables once 2 attestors confirm + cert ref present', fireBtnEnabled); await page.locator('button.btn-danger-solid').click(); - await page.waitForTimeout(300); + await page.waitForTimeout(600); const triggeredBannerVisible = await page.locator('.triggered-banner').isVisible(); record('Death Trigger: fires and shows triggered banner', triggeredBannerVisible); diff --git a/e2e-info.cjs b/e2e-info.cjs index 774b539..99694ef 100644 --- a/e2e-info.cjs +++ b/e2e-info.cjs @@ -1,5 +1,6 @@ // Verifies every tab has a working (i) info button that opens plain-language help. const { chromium } = require('playwright'); +const { signInFreshFamily } = require('./e2e-auth-helper.cjs'); const BASE = 'https://moslem04.falahos.my/'; const results = []; const consoleErrors = []; @@ -13,11 +14,11 @@ async function main() { page.on('console', m => { if (m.type() === 'error') consoleErrors.push(m.text()); }); page.on('pageerror', e => consoleErrors.push(e.message)); - await page.goto(BASE, { waitUntil: 'networkidle' }); + await signInFreshFamily(page, BASE, 'e2e-info'); for (const label of TABS) { await page.locator('nav button.tab', { hasText: label }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); const infoBtn = page.locator('.module-header .info-btn'); const btnVisible = await infoBtn.isVisible(); @@ -25,14 +26,14 @@ async function main() { if (!btnVisible) continue; await infoBtn.click(); - await page.waitForTimeout(150); + await page.waitForTimeout(500); const panelVisible = await page.locator('.info-panel').isVisible(); const whatText = await page.locator('.info-panel .info-section p').first().textContent().catch(() => ''); record(`"${label}": info panel opens with non-trivial explanation`, panelVisible && whatText.length > 40, `${whatText.length} chars`); // toggle closed await infoBtn.click(); - await page.waitForTimeout(150); + await page.waitForTimeout(500); const panelClosed = await page.locator('.info-panel').isVisible().catch(() => false); record(`"${label}": info panel closes on second click`, !panelClosed); } diff --git a/e2e-other.cjs b/e2e-other.cjs index b2de76d..2d8e51c 100644 --- a/e2e-other.cjs +++ b/e2e-other.cjs @@ -2,6 +2,7 @@ // previous bug: a nonsensical "EPF / Takaful nomination" suggestion pointing at a // channel value ('nomination') that didn't even exist in the Nomination Registry. const { chromium } = require('playwright'); +const { signInFreshFamily } = require('./e2e-auth-helper.cjs'); const BASE = 'https://moslem04.falahos.my/'; const results = []; const consoleErrors = []; @@ -13,10 +14,16 @@ async function main() { page.on('console', m => { if (m.type() === 'error') consoleErrors.push(m.text()); }); page.on('pageerror', e => consoleErrors.push(e.message)); - await page.goto(BASE, { waitUntil: 'networkidle' }); - const clickTab = async label => { await page.locator('nav button.tab', { hasText: label }).click(); await page.waitForTimeout(200); }; + await signInFreshFamily(page, BASE, 'e2e-other'); + const clickTab = async label => { await page.locator('nav button.tab', { hasText: label }).click(); await page.waitForTimeout(500); }; const selectByText = async (locator, text) => { - const val = await locator.evaluate((el, t) => Array.from(el.options).find(o => o.textContent.includes(t))?.value, text); + // Options load async from Supabase now — poll until the matching one shows up. + let val; + for (let i = 0; i < 20; i++) { + val = await locator.evaluate((el, t) => Array.from(el.options).find(o => o.textContent.includes(t))?.value, text); + if (val) break; + await new Promise(r => setTimeout(r, 300)); + } await locator.selectOption(val); }; @@ -25,7 +32,7 @@ async function main() { await page.locator('.field:has-text("Estimated value") input').fill('20000'); await page.locator('.form-card select').first().selectOption('Other'); await page.locator('button.btn-primary', { hasText: 'Add asset' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); await clickTab('Coverage'); const rowBefore = await page.locator('.asset-row', { hasText: 'stamp collection' }).textContent(); @@ -33,7 +40,7 @@ async function main() { await clickTab('Nominate'); await selectByText(page.locator('select').first(), 'stamp collection'); - await page.waitForTimeout(200); + await page.waitForTimeout(500); const suggestionText = await page.locator('.suggestion').textContent(); record('Nomination: suggestion for "Other" points to Hibah with sensible reasoning', suggestionText.includes('Hibah') && suggestionText.includes('safest general'), suggestionText.trim().slice(0, 200)); @@ -44,11 +51,11 @@ async function main() { await page.locator('.field:has-text("Asset / gift description") input').fill('Stamp collection gift'); await selectByText(page.locator('select').first(), 'stamp collection'); await page.locator('button.btn-primary', { hasText: 'Log this hibah' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); await page.locator('.guard-buttons button.btn-secondary', { hasText: 'No' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); await page.locator('button.btn-primary', { hasText: 'Confirm and save' }).click(); - await page.waitForTimeout(300); + await page.waitForTimeout(600); await clickTab('Coverage'); const pct = await page.locator('.big-percent').textContent(); diff --git a/e2e-property.cjs b/e2e-property.cjs index f724040..e253455 100644 --- a/e2e-property.cjs +++ b/e2e-property.cjs @@ -1,6 +1,7 @@ // Verifies property/land assets are covered through ALL THREE applicable fast-path // channels: Hibah, Waqf (Family Waqf Designator), and Trust (Nomination Registry). const { chromium } = require('playwright'); +const { signInFreshFamily } = require('./e2e-auth-helper.cjs'); const BASE = 'https://moslem04.falahos.my/'; const results = []; const consoleErrors = []; @@ -12,10 +13,16 @@ async function main() { page.on('console', m => { if (m.type() === 'error') consoleErrors.push(m.text()); }); page.on('pageerror', e => consoleErrors.push(e.message)); - await page.goto(BASE, { waitUntil: 'networkidle' }); - const clickTab = async label => { await page.locator('nav button.tab', { hasText: label }).click(); await page.waitForTimeout(200); }; + await signInFreshFamily(page, BASE, 'e2e-property'); + const clickTab = async label => { await page.locator('nav button.tab', { hasText: label }).click(); await page.waitForTimeout(500); }; const selectByText = async (locator, text) => { - const val = await locator.evaluate((el, t) => Array.from(el.options).find(o => o.textContent.includes(t))?.value, text); + // Options load async from Supabase now — poll until the matching one shows up. + let val; + for (let i = 0; i < 20; i++) { + val = await locator.evaluate((el, t) => Array.from(el.options).find(o => o.textContent.includes(t))?.value, text); + if (val) break; + await new Promise(r => setTimeout(r, 300)); + } await locator.selectOption(val); }; @@ -26,7 +33,7 @@ async function main() { await page.locator('.field:has-text("Estimated value") input').fill(val); // Type defaults to Property — leave as-is await page.locator('button.btn-primary', { hasText: 'Add asset' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); } await clickTab('Coverage'); @@ -42,11 +49,11 @@ async function main() { await page.locator('.field:has-text("Asset / gift description") input').fill('House gift'); await selectByText(page.locator('select').first(), 'House A'); await page.locator('button.btn-primary', { hasText: 'Log this hibah' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); await page.locator('.guard-buttons button.btn-secondary', { hasText: 'No' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); await page.locator('button.btn-primary', { hasText: 'Confirm and save' }).click(); - await page.waitForTimeout(300); + await page.waitForTimeout(600); await clickTab('Coverage'); const houseARow = await page.locator('.asset-row', { hasText: 'House A' }).textContent(); @@ -55,11 +62,11 @@ async function main() { // Channel 2: Waqf await clickTab('Family Waqf'); await selectByText(page.locator('select').first(), 'House B'); - await page.waitForTimeout(200); + await page.waitForTimeout(500); const corpusVal = await page.locator('select').first().inputValue(); record('Family Waqf: House B selectable as corpus asset', corpusVal !== ''); await page.locator('.field:has-text("Mutawalli (trustee)") input').fill('Ahmad bin Ismail'); - await page.waitForTimeout(300); // allow $effect to persist waqfCorpusAssetId + await page.waitForTimeout(600); // allow $effect to persist waqfCorpusAssetId await clickTab('Coverage'); const houseBRow = await page.locator('.asset-row', { hasText: 'House B' }).textContent(); @@ -68,15 +75,15 @@ async function main() { // Channel 3: Trust (Nomination Registry) await clickTab('Nominate'); await selectByText(page.locator('select').first(), 'House C'); - await page.waitForTimeout(200); + await page.waitForTimeout(500); await page.locator('.form-card select').nth(1).selectOption('trust'); - await page.waitForTimeout(200); + await page.waitForTimeout(500); const trustInputs = page.locator('.trust-fields .field input'); await trustInputs.nth(0).fill('Trustee Co'); await trustInputs.nth(1).fill('Successor Trustee Co'); await trustInputs.nth(2).fill('Equal split among children'); await page.locator('button.btn-primary', { hasText: 'Add trust setup' }).click(); - await page.waitForTimeout(300); + await page.waitForTimeout(600); await clickTab('Coverage'); const houseCRow = await page.locator('.asset-row', { hasText: 'House C' }).textContent(); @@ -94,9 +101,9 @@ async function main() { await page.locator('.attestor-row .confirm-btn').nth(1).click(); await page.locator('.field:has-text("Date of death") input').fill('2026-08-13'); await page.locator('.field:has-text("Death certificate reference") input').fill('DC-PROP-001'); - await page.waitForTimeout(200); + await page.waitForTimeout(500); await page.locator('button.btn-danger-solid').click(); - await page.waitForTimeout(300); + await page.waitForTimeout(600); const packetCount = await page.locator('.packet-row', { hasText: 'House' }).count(); record('Death Trigger: execution packets generated for all 3 property assets', packetCount === 3, `${packetCount} packets`); diff --git a/e2e-trust.cjs b/e2e-trust.cjs index 6179861..cb9ec6b 100644 --- a/e2e-trust.cjs +++ b/e2e-trust.cjs @@ -1,5 +1,6 @@ // Verifies land parcels specifically get covered via a trust setup in Nomination Registry. const { chromium } = require('playwright'); +const { signInFreshFamily } = require('./e2e-auth-helper.cjs'); const BASE = 'https://moslem04.falahos.my/'; const results = []; const consoleErrors = []; @@ -11,8 +12,8 @@ async function main() { page.on('console', m => { if (m.type() === 'error') consoleErrors.push(m.text()); }); page.on('pageerror', e => consoleErrors.push(e.message)); - await page.goto(BASE, { waitUntil: 'networkidle' }); - const clickTab = async label => { await page.locator('nav button.tab', { hasText: label }).click(); await page.waitForTimeout(200); }; + await signInFreshFamily(page, BASE, 'e2e-trust'); + const clickTab = async label => { await page.locator('nav button.tab', { hasText: label }).click(); await page.waitForTimeout(500); }; // Add a Property-type asset (land parcel) await clickTab('Assets'); @@ -20,7 +21,7 @@ async function main() { await page.locator('.field:has-text("Estimated value") input').fill('500000'); // type defaults to Property await page.locator('button.btn-primary', { hasText: 'Add asset' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); // Coverage should show this asset as exposed initially, with a trust suggestion await clickTab('Coverage'); @@ -35,13 +36,13 @@ async function main() { return opt ? opt.value : null; }); await assetSelect.selectOption(targetValue); - await page.waitForTimeout(200); + await page.waitForTimeout(500); const suggestionVisible = await page.locator('.suggestion').isVisible(); record('Nomination: trust suggestion shown for property-type asset', suggestionVisible); const typeSelect = page.locator('.form-card select').nth(1); await typeSelect.selectOption('trust'); - await page.waitForTimeout(200); + await page.waitForTimeout(500); const trustFieldsVisible = await page.locator('.trust-fields').isVisible(); record('Nomination: trust-specific fields (trustee/successor/beneficiaries) appear', trustFieldsVisible); @@ -50,7 +51,7 @@ async function main() { await trustInputs.nth(1).fill('Faridah binti Omar'); await trustInputs.nth(2).fill('Equal split among 3 children'); await page.locator('button.btn-primary', { hasText: 'Add trust setup' }).click(); - await page.waitForTimeout(300); + await page.waitForTimeout(600); const trustRowVisible = await page.locator('.nomination-row', { hasText: 'Land Parcel' }).isVisible(); record('Nomination: trust setup row created for land parcel', trustRowVisible); @@ -82,9 +83,9 @@ async function main() { await page.locator('.attestor-row .confirm-btn').nth(1).click(); await page.locator('.field:has-text("Date of death") input').fill('2026-08-13'); await page.locator('.field:has-text("Death certificate reference") input').fill('DC-TEST-001'); - await page.waitForTimeout(200); + await page.waitForTimeout(500); await page.locator('button.btn-danger-solid').click(); - await page.waitForTimeout(300); + await page.waitForTimeout(600); const packetForLand = await page.locator('.packet-row', { hasText: 'Land Parcel' }).isVisible(); record('Death Trigger: execution packet generated for land parcel via trust', packetForLand); diff --git a/e2e-uat.cjs b/e2e-uat.cjs index 9fed368..82c211f 100644 --- a/e2e-uat.cjs +++ b/e2e-uat.cjs @@ -1,6 +1,7 @@ // Full E2E UAT against the live moslem04.falahos.my deployment. // Simulates real human interaction: clicks, typed input, waits — not just DOM assertions. const { chromium } = require('playwright'); +const { signInFreshFamily } = require('./e2e-auth-helper.cjs'); const BASE = 'https://moslem04.falahos.my/'; const results = []; @@ -19,26 +20,26 @@ async function main() { page.on('pageerror', err => consoleErrors.push(err.message)); // ── Load ── - await page.goto(BASE, { waitUntil: 'networkidle' }); + await signInFreshFamily(page, BASE, 'e2e-uat'); record('Page loads', await page.title() === 'Nur Falah — Estate & Waqf Suite'); const tabs = ['Faraid', 'Assets', 'Wassiyah', 'Hibah', 'Family Waqf', 'Claims (H2)', 'Settings']; for (const label of tabs) { const tabBtn = page.locator('nav button.tab', { hasText: label }); await tabBtn.click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); const isActive = await tabBtn.evaluate(el => el.classList.contains('active')); record(`Nav: click "${label}" tab activates it`, isActive); } // ── Faraid Calculator: textbook case (wife + daughter + father + mother) ── await page.locator('nav button.tab', { hasText: 'Faraid' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); await page.locator('.field:has-text("Number of surviving wives") input').fill('1'); await page.locator('.field:has-text("Daughters") input').fill('1'); await page.locator('.field-check:has-text("Father survives") input').check(); await page.locator('.field-check:has-text("Mother survives") input').check(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); const shareRows = await page.locator('.share-row').allTextContents(); const hasWife = shareRows.some(r => r.includes('Wife') && r.includes('1/8')); const hasDaughter = shareRows.some(r => r.includes('Daughter') && r.includes('1/2')); @@ -48,12 +49,12 @@ async function main() { // ── Asset Registry: add an asset ── await page.locator('nav button.tab', { hasText: 'Assets' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); await page.locator('.field:has-text("Description") input').fill('Terrace house, Shah Alam'); await page.locator('.field:has-text("Estimated value") input').fill('600000'); await page.locator('.field:has-text("Ownership share") input').fill('100'); await page.locator('button.btn-primary', { hasText: 'Add asset' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); const total1 = await page.locator('.total-card strong').textContent(); record('Asset Registry: adding asset updates estate total', total1.includes('600,000'), total1); @@ -64,13 +65,13 @@ async function main() { await page.locator('.field:has-text("Description") input').fill('Savings account'); await page.locator('.field:has-text("Estimated value") input').fill('150000'); await page.locator('button.btn-primary', { hasText: 'Add asset' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); const total2 = await page.locator('.total-card strong').textContent(); record('Asset Registry: second asset accumulates total', total2.includes('750,000'), total2); // ── Wassiyah Generator: 1/3 meter + heir-exclusion block ── await page.locator('nav button.tab', { hasText: 'Wassiyah' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); const capText = await page.locator('.meter-row:has-text("One-third limit") strong').textContent(); record('Wassiyah: one-third meter reflects Asset Registry total (750,000/3=250,000)', capText.includes('250,000'), capText); @@ -79,16 +80,16 @@ async function main() { await page.locator('.form-card .field:has-text("Relation to you") input').fill('son'); await page.locator('.form-card .field:has-text("Description") input').fill('Cash gift'); await page.locator('.form-card .field:has-text("Value") input').fill('10000'); - await page.waitForTimeout(150); + await page.waitForTimeout(500); const blockErrorVisible = await page.locator('.block-error').isVisible(); const addBequestVisibleWhileBlocked = await page.locator('.form-card button.btn-primary', { hasText: 'Add bequest' }).isVisible().catch(() => false); record('Wassiyah: heir-relation bequest is blocked (no Add bequest button, error shown)', blockErrorVisible && !addBequestVisibleWhileBlocked); // Now a valid non-heir bequest await page.locator('.form-card .field:has-text("Relation to you") input').fill('nephew'); - await page.waitForTimeout(150); + await page.waitForTimeout(500); await page.locator('.form-card button.btn-primary', { hasText: 'Add bequest' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); const bequestRowVisible = await page.locator('.bequest-row', { hasText: 'My Son' }).isVisible(); record('Wassiyah: valid non-heir bequest is added', bequestRowVisible); @@ -97,53 +98,56 @@ async function main() { await page.locator('.form-card .field:has-text("Relation to you") input').fill('charity'); await page.locator('.form-card .field:has-text("Description") input').fill('Endowment gift'); await page.locator('.form-card .field:has-text("Value") input').fill('280000'); - await page.waitForTimeout(150); + await page.waitForTimeout(500); await page.locator('.form-card button.btn-primary', { hasText: 'Add bequest' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); const overrideBoxVisible = await page.locator('.override-box').isVisible(); 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); await page.locator('.override-box input[type=checkbox]').check(); - await page.waitForTimeout(150); + await page.waitForTimeout(500); const exportEnabledAfterAck = await page.locator('button.btn-primary', { hasText: 'Export draft' }).isEnabled(); record('Wassiyah: acknowledging override enables export', exportEnabledAfterAck); // ── Hibah Tracker: marad al-mawt guard, blocked-heir path ── await page.locator('nav button.tab', { hasText: 'Hibah' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); await page.locator('.field:has-text("Recipient") input').fill('My Daughter'); await page.locator('.field:has-text("Relation to you") input').fill('daughter'); await page.locator('.field:has-text("Asset / gift description") input').fill('Car'); await page.locator('button.btn-primary', { hasText: 'Log this hibah' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); const guardQuestionVisible = await page.locator('.guard-question').isVisible(); record('Hibah: marad al-mawt guard question appears on new entry', guardQuestionVisible); await page.locator('.guard-buttons button.btn-warn', { hasText: 'Yes' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); const guardErrorVisible = await page.locator('.guard-error').isVisible(); const confirmDisabled = await page.locator('button.btn-primary', { hasText: 'Confirm and save' }).isDisabled(); record('Hibah: flagged + heir beneficiary blocks confirm', guardErrorVisible && confirmDisabled, 'recipient=daughter, flagged=yes'); // ── Family Waqf Designator: open note + beneficiary flow ── await page.locator('nav button.tab', { hasText: 'Family Waqf' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); const openNoteVisible = await page.locator('.open-note').isVisible(); record('Family Waqf: open fiqh-question note is visible (OPEN-01 transparency)', openNoteVisible); const corpusSelect = page.locator('select').first(); + // Data now loads async from Supabase (was synchronous localStorage before) — + // wait for the fetched options to actually land instead of a fixed timeout. + await corpusSelect.locator('option').nth(1).waitFor({ state: 'attached', timeout: 10000 }).catch(() => {}); const optionCount = await corpusSelect.locator('option').count(); await corpusSelect.selectOption({ index: 1 }); // index 0 is the placeholder const corpusSelected = await corpusSelect.inputValue(); record('Family Waqf: corpus asset dropdown is populated from Asset Registry', optionCount > 1 && corpusSelected !== '', `${optionCount} options, selected="${corpusSelected}"`); await page.locator('.field:has-text("Mutawalli (trustee)") input').fill('Ahmad bin Ismail'); - await page.waitForTimeout(150); + await page.waitForTimeout(500); // ── Horizon 2 Claims: pre-pilot banner + issue + transfer restriction ── await page.locator('nav button.tab', { hasText: 'Claims (H2)' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); const pilotBannerVisible = await page.locator('.pilot-banner').isVisible(); const bannerText = await page.locator('.pilot-banner').textContent(); record('Claims: pre-pilot banner visible and mentions Phase 0', pilotBannerVisible && bannerText.includes('Phase 0')); @@ -153,7 +157,7 @@ async function main() { await page.locator('.form-card .field:has-text("Holder name") input').fill('Aisyah binti Ahmad'); await page.locator('.form-card .field:has-text("Heir share fraction") input').fill('1/8'); await page.locator('.form-card button.btn-primary', { hasText: 'Issue demo claim' }).click(); - await page.waitForTimeout(300); + await page.waitForTimeout(600); const claimCardVisible = await page.locator('.claim-card', { hasText: 'Lot 42' }).isVisible(); record('Claims: issuing a demo claim creates a claim card', claimCardVisible); @@ -161,7 +165,7 @@ async function main() { const claimCard = page.locator('.claim-card', { hasText: 'Lot 42' }); await claimCard.locator('.transfer-row input').fill('Random Outsider'); await claimCard.locator('.btn-small', { hasText: 'Transfer within pool' }).click(); - await page.waitForTimeout(300); + await page.waitForTimeout(600); // Note: our transfer flow defaults toHeirPoolId to the claim's own pool when not overridden, // so this exercises the success path; the code-level restriction is verified separately below. const statusAfterTransfer = await claimCard.locator('.status').textContent(); @@ -169,33 +173,33 @@ async function main() { // ── Settings: export + delete-all guarded by confirm() ── await page.locator('nav button.tab', { hasText: 'Settings' }).click(); - await page.waitForTimeout(200); - const exportBtnVisible = await page.locator('button.btn-secondary', { hasText: 'Export all data' }).isVisible(); - const deleteBtnVisible = await page.locator('button.btn-danger', { hasText: 'Delete all data' }).isVisible(); + await page.waitForTimeout(500); + const exportBtnVisible = await page.locator('button.btn-secondary', { hasText: 'Export local export' }).isVisible(); + const deleteBtnVisible = await page.locator('button.btn-danger', { hasText: 'Delete local device data' }).isVisible(); record('Settings: export and delete-all controls present', exportBtnVisible && deleteBtnVisible); // Export download check const [download] = await Promise.all([ page.waitForEvent('download'), - page.locator('button.btn-secondary', { hasText: 'Export all data' }).click() + page.locator('button.btn-secondary', { hasText: 'Export local export' }).click() ]); record('Settings: export triggers a real file download', download.suggestedFilename() === 'nur-falah-full-export.json', download.suggestedFilename()); // Delete-all: dismiss the confirm() dialog first (verify guard exists), then accept and verify wipe page.once('dialog', async d => { record('Settings: delete-all is guarded by a confirm() dialog', d.type() === 'confirm'); await d.dismiss(); }); - await page.locator('button.btn-danger', { hasText: 'Delete all data' }).click(); - await page.waitForTimeout(200); + await page.locator('button.btn-danger', { hasText: 'Delete local device data' }).click(); + await page.waitForTimeout(500); await page.locator('nav button.tab', { hasText: 'Assets' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); const dataStillThereAfterDismiss = await page.locator('.asset-row', { hasText: 'Terrace house' }).isVisible(); record('Settings: dismissing delete confirm leaves data intact', dataStillThereAfterDismiss); // ── Bilingual: language switcher click actually changes visible UI text ── await page.locator('nav button.tab', { hasText: 'Settings' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); const taglineBefore = await page.locator('.header-tagline').textContent(); await page.locator('.lang-btn', { hasText: 'Bahasa Malaysia' }).click(); - await page.waitForTimeout(200); + await page.waitForTimeout(500); const taglineAfter = await page.locator('.header-tagline').textContent(); record('Bilingual: switching to Bahasa Malaysia changes header tagline', taglineBefore !== taglineAfter && taglineAfter.includes('WAKAF'), `"${taglineBefore}" -> "${taglineAfter}"`); await page.locator('.lang-btn', { hasText: 'English' }).click(); diff --git a/src/lib/WassiyahGenerator.svelte b/src/lib/WassiyahGenerator.svelte index b23250f..d888b1c 100644 --- a/src/lib/WassiyahGenerator.svelte +++ b/src/lib/WassiyahGenerator.svelte @@ -1,34 +1,58 @@