2549e9de0c
Big round covering two different asks: Khairat/emergency infrastructure (tightly coupled to what's already built) and a Muslim-lifestyle companion surface (a genuinely different product, added at the user's explicit request after being offered a smaller scope). Khairat & emergency contacts: - nf_khairat_memberships (per-member scheme membership) + nf_emergency_fund (per-family shared reserve tracker) — records intent/contacts only, never holds or moves real money. - nf_trusted_contacts gains category (mosque/police/ambulance/hospital/ khairat/family/other) and email columns. - New notify-emergency-contacts Edge Function, auto-invoked the moment a mutawalli fires a member's death trigger (the one place in this app where auto-send-on-trigger is actually correct), alongside — not instead of — the existing heir notification. Verified with a live fire end-to-end (e2e-emergency-notify.cjs, 5/5). Lifestyle-companion tabs — all computed/searched live, nothing fabricated: - Qibla: pure great-circle bearing math to the Kaaba from geolocation, no API/key. Verified against Kuala Lumpur (293°, matches expected ~292°). - Prayer Times: client-side astronomical calculation (single-pass solar position, MWL angles), sanity-checked against known KL/London times before shipping. - Locate: mosque/halal/cemetery search via the free, keyless OpenStreetMap Overpass API — real community-sourced results only, honest empty state when nothing's mapped nearby. - Quran: Surah list + Arabic/translation via the free alquran.cloud API, fetched fresh each time, nothing stored in this app's own database. - Neighbourhood: a join-by-code community announcement board — a genuinely separate multi-tenant concept from the estate-planning family structure, scoped to the user account. Found and fixed a real UX gap during testing: the create/join form was only reachable with zero existing neighbourhoods, with no way to join a second one. Also found and fixed a real bug: the heir-notify and emergency-notify status messages shared the same CSS class, breaking any script (including the pre-existing e2e-per-member.cjs) that targeted '.notify-status' without further filtering — gave each its own distinguishing class. Covered by e2e-khairat-lifestyle.cjs (13/13) and e2e-emergency-notify.cjs (5/5). Full regression: 316/316 across all suites (several transient flakes under heavy mail-relay load during the sweep, all confirmed clean on rerun — one led to the real class-collision fix above).
89 lines
5.3 KiB
JavaScript
89 lines
5.3 KiB
JavaScript
// Verifies that firing a member's death trigger automatically invokes the
|
|
// notify-emergency-contacts Edge Function (mosque/khairat/police/ambulance
|
|
// contacts) alongside the existing heir notification — the one place in
|
|
// this app where "auto-send on trigger" is actually the right behavior.
|
|
const { chromium } = require('playwright');
|
|
const BASE = 'https://moslem04.falahos.my/';
|
|
const OWNER = 'nurfalah.e2etest.owner@gmail.com';
|
|
const AGENT = 'nurfalah.e2etest.agent@gmail.com';
|
|
const PASSWORD = 'TestPassword123!';
|
|
const results = [];
|
|
function record(name, pass, detail = '') { results.push({ name, pass, detail }); console.log(`${pass ? 'PASS' : 'FAIL'} ${name}${detail ? ' — ' + detail : ''}`); }
|
|
|
|
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 familyName = `EmergencyNotify ${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
|
|
const browser = await chromium.launch();
|
|
|
|
const ownerCtx = await browser.newContext({ viewport: { width: 390, height: 844 } });
|
|
const ownerPage = await ownerCtx.newPage();
|
|
await signIn(ownerPage, OWNER);
|
|
await ownerPage.locator('.field:has-text("Family name") input').fill(familyName);
|
|
await ownerPage.locator('button.btn-primary', { hasText: 'Create family' }).click();
|
|
await ownerPage.waitForTimeout(1200);
|
|
|
|
await ownerPage.locator('nav button[aria-label="Assets"]').click();
|
|
await ownerPage.waitForTimeout(500);
|
|
await ownerPage.locator('.contacts-section .field:has-text("Name") input').fill('Emergency Test Mosque');
|
|
await ownerPage.locator('.contacts-section .field:has-text("Category") select').selectOption('mosque');
|
|
await ownerPage.locator('.contacts-section .field:has-text("Email") input').fill('mosque-emergency-test@example.com');
|
|
await ownerPage.locator('.contacts-section button.btn-secondary', { hasText: 'Add trusted contact' }).click();
|
|
await ownerPage.waitForTimeout(1000);
|
|
record('Setup: mosque contact with email added', await ownerPage.locator('.contact-row', { hasText: 'Emergency Test Mosque' }).isVisible().catch(() => false));
|
|
|
|
await ownerPage.locator('nav button[aria-label="Family"]').click();
|
|
await ownerPage.waitForTimeout(500);
|
|
await ownerPage.locator('.field:has-text("Invite by email") input').fill(AGENT);
|
|
await ownerPage.locator('.field:has-text("Role") select').selectOption('agent');
|
|
await ownerPage.locator('button.btn-primary', { hasText: 'Send invite' }).click();
|
|
await ownerPage.waitForTimeout(1000);
|
|
|
|
const agentCtx = await browser.newContext({ viewport: { width: 390, height: 844 } });
|
|
const agentPage = await agentCtx.newPage();
|
|
let notifyStatus = null;
|
|
agentPage.on('response', r => { if (r.url().includes('functions/v1/notify-emergency-contacts')) notifyStatus = r.status(); });
|
|
await signIn(agentPage, AGENT);
|
|
const pending = agentPage.locator('.invite-row', { hasText: familyName });
|
|
if (await pending.count()) { await pending.locator('button', { hasText: 'Accept' }).click(); await agentPage.waitForTimeout(1000); }
|
|
const familyRow = agentPage.locator('.family-row', { hasText: familyName });
|
|
if (await familyRow.count()) { await familyRow.click(); await agentPage.waitForTimeout(1000); }
|
|
|
|
await agentPage.locator('nav button[aria-label="Mutawalli"]').click();
|
|
await agentPage.waitForTimeout(1000);
|
|
const memberChip = agentPage.locator('.member-chip', { hasText: 'nurfalah.e2etest.owner' });
|
|
record('Mutawalli: owner appears as a member to fire a trigger for', await memberChip.count() === 1);
|
|
if (await memberChip.count()) await memberChip.click();
|
|
await agentPage.waitForTimeout(500);
|
|
|
|
const attestorInputs = agentPage.locator('.attestor-row input');
|
|
await attestorInputs.nth(0).fill('Witness A');
|
|
await agentPage.locator('.attestor-row .confirm-btn').nth(0).click();
|
|
await attestorInputs.nth(1).fill('Witness B');
|
|
await agentPage.locator('.attestor-row .confirm-btn').nth(1).click();
|
|
await agentPage.locator('.field:has-text("Date of death") input').fill('2026-01-01');
|
|
await agentPage.locator('.field:has-text("Death certificate") input').fill(`CERT-${Date.now()}`);
|
|
await agentPage.waitForTimeout(500);
|
|
|
|
await agentPage.locator('button.btn-danger-solid').click();
|
|
await agentPage.waitForTimeout(4000);
|
|
|
|
record('Trigger: fires successfully (triggered banner shown)', await agentPage.locator('.triggered-banner').isVisible().catch(() => false));
|
|
record('Emergency notify: auto-invoked on fire, returns 200', notifyStatus === 200, `status: ${notifyStatus}`);
|
|
record('Emergency notify: status message shown to the mutawalli', await agentPage.locator('.emergency-notify-status', { hasText: 'Notified' }).isVisible().catch(() => false));
|
|
|
|
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); });
|