refactor: restructure navigation into 5 grouped hubs, off the 22-item flat strip
Implements the researched IA fix: mobile nav UX consensus caps primary destinations at 4-5 (uxpin.com, fintech/banking 2026 UX research), and this app had grown to 22 tabs in one horizontally-scrolling row — exactly the anti-pattern that research flags for choice paralysis and slower task completion. New structure — 5 bottom-nav hubs, grouped by what the user is actually trying to do, not build order: - Home: Coverage (unchanged, still the landing screen) - Estate: Assets, Faraid, Insurance, Wassiyah, Hibah, Family Waqf, Nominate, Claims (H2) - Giving: Zakat, Sadaqah, Khairat - Family: Tree, Trigger, Mutawalli, Manage (was 'Family', renamed to avoid colliding with the hub's own label), Neighbourhood - Daily: Prayer Times, Qibla, Quran, Locate Each hub reveals its own sub-nav one tap in, instead of every tab competing for space in a single row. Settings moved out of the tab strip entirely into a header gear icon, matching the banking-app pattern of keeping settings out of primary thumb-reach real estate. The bottom nav is now fixed (thumb-zone), sub-nav keeps the old sticky-top position. Cross-component navigation (nav.js requestedTab, used by the Family Tree's 'give sadaqah in memory' link) now resolves a tab label to its owning (hub, sub-tab) pair instead of a flat index — verified live. This touches every E2E suite: a single click on a tab's old selector no longer reaches it (hub, then sub-tab). Added a shared gotoTab(page, label) helper to e2e-auth-helper.cjs encapsulating the two-step navigation, and migrated all ~22 affected test files off direct nav-button selectors — mechanical substitution followed by manual fixes for local clickTab wrappers, template-literal selectors, and active-state assertions that needed to target the new .hub-tab/.subnav structure specifically. Full regression after migration: every suite passes (one isolated Family Tree flake confirmed clean on rerun, unrelated to navigation).
This commit is contained in:
+34
-1
@@ -22,4 +22,37 @@ async function signInFreshFamily(page, BASE, familyLabel) {
|
||||
return familyName;
|
||||
}
|
||||
|
||||
module.exports = { signInFreshFamily, OWNER_EMAIL, OWNER_PASSWORD };
|
||||
// Hub map mirroring App.svelte's HUBS config — the nav went from a single
|
||||
// flat 22-item strip to 5 bottom-nav hubs, each revealing its own sub-nav.
|
||||
// Reaching any given tab is now a two-step click (hub, then sub-tab)
|
||||
// instead of one, so every test goes through this helper rather than
|
||||
// hardcoding the old single-click selector.
|
||||
const HUB_OF_TAB = {
|
||||
'Coverage': 'Home',
|
||||
'Assets': 'Estate', 'Faraid': 'Estate', 'Insurance': 'Estate', 'Wassiyah': 'Estate',
|
||||
'Hibah': 'Estate', 'Family Waqf': 'Estate', 'Nominate': 'Estate', 'Claims (H2)': 'Estate',
|
||||
'Zakat': 'Giving', 'Sadaqah': 'Giving', 'Khairat': 'Giving',
|
||||
'Tree': 'Family', 'Trigger': 'Family', 'Mutawalli': 'Family', 'Manage': 'Family', 'Neighbourhood': 'Family',
|
||||
'Prayer Times': 'Daily', 'Qibla': 'Daily', 'Quran': 'Daily', 'Locate': 'Daily'
|
||||
};
|
||||
|
||||
/** Navigates to a tab by its old flat label — clicks the owning hub first (if not already active), then the sub-tab. 'Settings' is a special case (a header icon, not a hub). */
|
||||
async function gotoTab(page, label) {
|
||||
if (label === 'Settings') {
|
||||
await page.locator('button[aria-label="Settings"]').click();
|
||||
return;
|
||||
}
|
||||
const hub = HUB_OF_TAB[label];
|
||||
if (!hub) throw new Error(`gotoTab: unknown tab label "${label}"`);
|
||||
const hubBtn = page.locator(`.hub-tab[aria-label="${hub}"]`);
|
||||
const alreadyActive = await hubBtn.evaluate(el => el.classList.contains('active')).catch(() => false);
|
||||
if (!alreadyActive) {
|
||||
await hubBtn.click();
|
||||
await page.waitForTimeout(200);
|
||||
}
|
||||
if (hub !== 'Home') {
|
||||
await page.locator(`.subnav .tab[aria-label="${label}"]`).click();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { signInFreshFamily, OWNER_EMAIL, OWNER_PASSWORD, gotoTab };
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
const { chromium } = require('playwright');
|
||||
const { gotoTab } = require('./e2e-auth-helper.cjs');
|
||||
const BASE = 'https://moslem04.falahos.my/';
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 390, height: 844 } });
|
||||
await page.goto(BASE, { waitUntil: 'networkidle' });
|
||||
const clickTab = async label => { await page.locator('nav button.tab', { hasText: label }).click(); await page.waitForTimeout(200); };
|
||||
const clickTab = async label => { await gotoTab(page, label); await page.waitForTimeout(200); };
|
||||
|
||||
await clickTab('Assets');
|
||||
await page.locator('.field:has-text("Description") input').fill('Bakery Sdn Bhd shares');
|
||||
|
||||
+2
-2
@@ -1,7 +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 { signInFreshFamily, gotoTab } = require('./e2e-auth-helper.cjs');
|
||||
const BASE = 'https://moslem04.falahos.my/';
|
||||
const results = [];
|
||||
const consoleErrors = [];
|
||||
@@ -14,7 +14,7 @@ async function main() {
|
||||
page.on('pageerror', e => consoleErrors.push(e.message));
|
||||
|
||||
await signInFreshFamily(page, BASE, 'e2e-business');
|
||||
const clickTab = async label => { await page.locator('nav button.tab', { hasText: label }).click(); await page.waitForTimeout(500); };
|
||||
const clickTab = async label => { await gotoTab(page, label); await page.waitForTimeout(500); };
|
||||
const selectByText = async (locator, text) => {
|
||||
// Options load async from Supabase now — poll until the matching one shows up.
|
||||
let val;
|
||||
|
||||
@@ -2,7 +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 { signInFreshFamily, gotoTab } = require('./e2e-auth-helper.cjs');
|
||||
const BASE = 'https://moslem04.falahos.my/';
|
||||
const results = [];
|
||||
const consoleErrors = [];
|
||||
@@ -15,7 +15,7 @@ async function main() {
|
||||
page.on('pageerror', e => consoleErrors.push(e.message));
|
||||
|
||||
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 clickTab = async label => { await gotoTab(page, label); await page.waitForTimeout(500); };
|
||||
const selectByText = async (locator, text) => {
|
||||
// Options load async from Supabase now — poll until the matching one shows up.
|
||||
let val;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// 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 { gotoTab } = require('./e2e-auth-helper.cjs');
|
||||
const BASE = 'https://moslem04.falahos.my/';
|
||||
const OWNER = 'nurfalah.e2etest.owner@gmail.com';
|
||||
const AGENT = 'nurfalah.e2etest.agent@gmail.com';
|
||||
@@ -29,7 +30,7 @@ async function main() {
|
||||
await ownerPage.locator('button.btn-primary', { hasText: 'Create family' }).click();
|
||||
await ownerPage.waitForTimeout(1200);
|
||||
|
||||
await ownerPage.locator('nav button[aria-label="Assets"]').click();
|
||||
await gotoTab(ownerPage, 'Assets');
|
||||
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');
|
||||
@@ -38,7 +39,7 @@ async function main() {
|
||||
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 gotoTab(ownerPage, 'Manage');
|
||||
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');
|
||||
@@ -55,7 +56,7 @@ async function main() {
|
||||
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 gotoTab(agentPage, 'Mutawalli');
|
||||
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);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// 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 { gotoTab } = require('./e2e-auth-helper.cjs');
|
||||
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 : ''}`); }
|
||||
@@ -60,10 +61,10 @@ async function main() {
|
||||
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);
|
||||
const onMainApp = await ownerPage.locator('.hub-tab[aria-label="Home"]').isVisible().catch(() => false);
|
||||
record('Owner: creating a family lands on the main app', onMainApp);
|
||||
|
||||
await ownerPage.locator('nav button[aria-label="Family"]').click();
|
||||
await gotoTab(ownerPage, 'Manage');
|
||||
await ownerPage.waitForTimeout(300);
|
||||
await ownerPage.locator('.field:has-text("Invite by email") input').fill(agentEmail);
|
||||
await ownerPage.locator('.field:has-text("Role") select').selectOption('agent');
|
||||
@@ -81,10 +82,10 @@ async function main() {
|
||||
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);
|
||||
const agentOnMainApp = await agentPage.locator('.hub-tab[aria-label="Home"]').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 gotoTab(agentPage, 'Assets');
|
||||
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');
|
||||
@@ -93,12 +94,12 @@ async function main() {
|
||||
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();
|
||||
await gotoTab(ownerPage, 'Assets');
|
||||
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();
|
||||
await gotoTab(agentPage, 'Trigger');
|
||||
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);
|
||||
|
||||
@@ -117,7 +118,7 @@ async function main() {
|
||||
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 gotoTab(ownerPage, 'Trigger');
|
||||
await ownerPage.waitForTimeout(1000);
|
||||
const ownerAttestorInputs = ownerPage.locator('.attestor-row input');
|
||||
const attestorCount = await ownerAttestorInputs.count();
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@
|
||||
// proven for Wassiyah/Waqf).
|
||||
const { chromium } = require('playwright');
|
||||
const path = require('path');
|
||||
const { signInFreshFamily } = require('./e2e-auth-helper.cjs');
|
||||
const { signInFreshFamily, gotoTab } = require('./e2e-auth-helper.cjs');
|
||||
const BASE = 'https://moslem04.falahos.my/';
|
||||
const results = [];
|
||||
const consoleErrors = [];
|
||||
@@ -19,7 +19,7 @@ async function main() {
|
||||
page.on('pageerror', e => consoleErrors.push(e.message));
|
||||
|
||||
const familyName = await signInFreshFamily(page, BASE, 'e2e-family-tree');
|
||||
await page.locator('nav button[aria-label="Tree"]').click();
|
||||
await gotoTab(page, 'Tree');
|
||||
await page.waitForTimeout(600);
|
||||
|
||||
// Add three people: grandfather, father, son
|
||||
@@ -109,7 +109,7 @@ async function main() {
|
||||
// ── Isolation: a fresh family (same account) should NOT see this tree ──
|
||||
const isolationPage = await browser.newPage({ viewport: { width: 390, height: 844 } });
|
||||
await signInFreshFamily(isolationPage, BASE, 'e2e-family-tree-isolation');
|
||||
await isolationPage.locator('nav button[aria-label="Tree"]').click();
|
||||
await gotoTab(isolationPage, 'Tree');
|
||||
await isolationPage.waitForTimeout(600);
|
||||
const leakedPerson = await isolationPage.locator('.person-card', { hasText: 'Grandfather Ahmad' }).isVisible().catch(() => false);
|
||||
record('Family Tree: a different family sees none of this tree (isolation)', !leakedPerson);
|
||||
|
||||
+9
-5
@@ -1,7 +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 { signInFreshFamily, gotoTab } = require('./e2e-auth-helper.cjs');
|
||||
const BASE = 'https://moslem04.falahos.my/';
|
||||
const results = [];
|
||||
const consoleErrors = [];
|
||||
@@ -15,12 +15,16 @@ async function main() {
|
||||
|
||||
await signInFreshFamily(page, BASE, 'e2e-fastpath');
|
||||
|
||||
const clickTab = async label => { await page.locator('nav button.tab', { hasText: label }).click(); await page.waitForTimeout(500); };
|
||||
const clickTab = async label => { await gotoTab(page, label); await page.waitForTimeout(500); };
|
||||
|
||||
// New tabs exist
|
||||
// New tabs exist and are reachable
|
||||
for (const label of ['Coverage', 'Nominate', 'Trigger']) {
|
||||
const visible = await page.locator('nav button.tab', { hasText: label }).isVisible();
|
||||
record(`Nav: "${label}" tab exists`, visible);
|
||||
await gotoTab(page, label);
|
||||
await page.waitForTimeout(300);
|
||||
const reached = label === 'Coverage'
|
||||
? await page.locator('.hub-tab.active[aria-label="Home"]').isVisible().catch(() => false)
|
||||
: await page.locator('.subnav .tab.active', { hasText: label }).isVisible().catch(() => false);
|
||||
record(`Nav: "${label}" tab exists`, reached);
|
||||
}
|
||||
|
||||
// Add an asset
|
||||
|
||||
+5
-5
@@ -3,7 +3,7 @@
|
||||
// madhab selector (including honest Ja'fari gating), and the Wassiyah tab's
|
||||
// draft will document generator.
|
||||
const { chromium } = require('playwright');
|
||||
const { signInFreshFamily } = require('./e2e-auth-helper.cjs');
|
||||
const { signInFreshFamily, gotoTab } = require('./e2e-auth-helper.cjs');
|
||||
const BASE = 'https://moslem04.falahos.my/';
|
||||
const results = [];
|
||||
const consoleErrors = [];
|
||||
@@ -28,14 +28,14 @@ async function main() {
|
||||
record('Coverage: flags missing estate agent/mutawalli', noAgentRec);
|
||||
|
||||
// Add an asset to trigger the exposed-asset recommendation
|
||||
await page.locator('nav button[aria-label="Assets"]').click();
|
||||
await gotoTab(page, 'Assets');
|
||||
await page.waitForTimeout(500);
|
||||
await page.locator('.form-card .field:has-text("Description") input').fill('Savings account');
|
||||
await page.locator('.form-card .field:has-text("Estimated value") input').fill('50000');
|
||||
await page.locator('.form-card button.btn-primary', { hasText: 'Add asset' }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.locator('nav button[aria-label="Coverage"]').click();
|
||||
await gotoTab(page, 'Coverage');
|
||||
await page.waitForTimeout(800);
|
||||
const exposedRec = await page.locator('.rec-row', { hasText: 'exposed to the slow' }).isVisible().catch(() => false);
|
||||
record('Coverage: flags exposed asset once one is logged', exposedRec);
|
||||
@@ -43,7 +43,7 @@ async function main() {
|
||||
record('Coverage: flags missing Wassiyah once estate has value', wassiyahRec);
|
||||
|
||||
// ── Faraid Calculator: madhab selector ──
|
||||
await page.locator('nav button[aria-label="Faraid"]').click();
|
||||
await gotoTab(page, 'Faraid');
|
||||
await page.waitForTimeout(500);
|
||||
const madhabSelectVisible = await page.locator('.field:has-text("Madhab") select').isVisible().catch(() => false);
|
||||
record('Faraid: madhab selector is present', madhabSelectVisible);
|
||||
@@ -72,7 +72,7 @@ async function main() {
|
||||
|
||||
// ── Wassiyah: draft will document generator ──
|
||||
await page.locator('.field:has-text("Madhab") select').selectOption('shafii'); // reset, avoid cross-test pollution
|
||||
await page.locator('nav button[aria-label="Wassiyah"]').click();
|
||||
await gotoTab(page, 'Wassiyah');
|
||||
await page.waitForTimeout(500);
|
||||
await page.locator('.field:has-text("Full legal name") input').fill('Ahmad bin Ismail');
|
||||
await page.locator('.field:has-text("Recipient name") input').fill('Nur Charity Foundation');
|
||||
|
||||
+2
-2
@@ -1,6 +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 { signInFreshFamily, gotoTab } = require('./e2e-auth-helper.cjs');
|
||||
const BASE = 'https://moslem04.falahos.my/';
|
||||
const results = [];
|
||||
const consoleErrors = [];
|
||||
@@ -17,7 +17,7 @@ async function main() {
|
||||
await signInFreshFamily(page, BASE, 'e2e-info');
|
||||
|
||||
for (const label of TABS) {
|
||||
await page.locator('nav button.tab', { hasText: label }).click();
|
||||
await gotoTab(page, label);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const infoBtn = page.locator('.module-header .info-btn');
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// per-member Insurance/Takaful policies, family-shared Liabilities, and
|
||||
// asset-ownership verification (proof document + dual-path confirm).
|
||||
const { chromium } = require('playwright');
|
||||
const { signInFreshFamily } = require('./e2e-auth-helper.cjs');
|
||||
const { signInFreshFamily, gotoTab } = require('./e2e-auth-helper.cjs');
|
||||
const BASE = 'https://moslem04.falahos.my/';
|
||||
const results = [];
|
||||
const consoleErrors = [];
|
||||
@@ -17,7 +17,7 @@ async function main() {
|
||||
await signInFreshFamily(page, BASE, 'e2e-insurance');
|
||||
|
||||
// ── Insurance & Takaful tab ──
|
||||
await page.locator('nav button[aria-label="Insurance"]').click();
|
||||
await gotoTab(page, 'Insurance');
|
||||
await page.waitForTimeout(600);
|
||||
|
||||
await page.locator('.form-card .field:has-text("Type") select').selectOption('takaful');
|
||||
@@ -44,7 +44,7 @@ async function main() {
|
||||
record('Insurance: editing a policy succeeds without error', consoleErrors.length === 0, consoleErrors.join(' || '));
|
||||
|
||||
// ── Asset ownership verification (Assets tab) ──
|
||||
await page.locator('nav button[aria-label="Assets"]').click();
|
||||
await gotoTab(page, 'Assets');
|
||||
await page.waitForTimeout(600);
|
||||
await page.locator('.form-card .field:has-text("Description") input').fill('Family sedan');
|
||||
await page.locator('.form-card .field:has-text("Estimated value") input').fill('45000');
|
||||
@@ -85,7 +85,7 @@ async function main() {
|
||||
record('Liabilities: removing a liability removes it from the list', !liabilityRemoved);
|
||||
|
||||
// ── Mutawalli dashboard should surface the insurance policy for this member (owner-only family: owner sees their own row) ──
|
||||
await page.locator('nav button[aria-label="Mutawalli"]').click();
|
||||
await gotoTab(page, 'Mutawalli');
|
||||
await page.waitForTimeout(800);
|
||||
const mutawalliText = await page.locator('.module').innerText().catch(() => '');
|
||||
const mutawalliGated = mutawalliText.includes('Only the mutawalli');
|
||||
@@ -94,7 +94,7 @@ async function main() {
|
||||
// ── Isolation: a second fresh family (same account) must not see this policy/liability/verification data ──
|
||||
const isolationPage = await browser.newPage({ viewport: { width: 390, height: 844 } });
|
||||
await signInFreshFamily(isolationPage, BASE, 'e2e-insurance-isolation');
|
||||
await isolationPage.locator('nav button[aria-label="Insurance"]').click();
|
||||
await gotoTab(isolationPage, 'Insurance');
|
||||
await isolationPage.waitForTimeout(600);
|
||||
const leakedPolicy = await isolationPage.locator('.policy-row', { hasText: 'Etiqa Takaful' }).isVisible().catch(() => false);
|
||||
record('Isolation: a different family sees none of this insurance data', !leakedPolicy);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// effects on Wassiyah/will document/Zakat currency, and the professional
|
||||
// review request workflow against the live backend.
|
||||
const { chromium } = require('playwright');
|
||||
const { signInFreshFamily } = require('./e2e-auth-helper.cjs');
|
||||
const { signInFreshFamily, gotoTab } = require('./e2e-auth-helper.cjs');
|
||||
const BASE = 'https://moslem04.falahos.my/';
|
||||
const results = [];
|
||||
const consoleErrors = [];
|
||||
@@ -17,7 +17,7 @@ async function main() {
|
||||
await signInFreshFamily(page, BASE, 'e2e-jurisdiction');
|
||||
|
||||
// ── Settings: jurisdiction selector, owner can change ──
|
||||
await page.locator('nav button[aria-label="Settings"]').click();
|
||||
await gotoTab(page, 'Settings');
|
||||
await page.waitForTimeout(600);
|
||||
const jurisdictionSelectVisible = await page.locator('.jurisdiction-setting select').isVisible().catch(() => false);
|
||||
record('Settings: jurisdiction selector visible for owner', jurisdictionSelectVisible);
|
||||
@@ -28,7 +28,7 @@ async function main() {
|
||||
record('Settings: changing jurisdiction shows a saved confirmation', savedBadge);
|
||||
|
||||
// ── Zakat: currency defaults follow the family jurisdiction (SGD) ──
|
||||
await page.locator('nav button[aria-label="Zakat"]').click();
|
||||
await gotoTab(page, 'Zakat');
|
||||
await page.waitForTimeout(800);
|
||||
const sgdLabelVisible = await page.locator('.field:has-text("Cash")').textContent();
|
||||
record('Zakat: currency label reflects Singapore jurisdiction (SGD)', sgdLabelVisible.includes('SGD'), sgdLabelVisible);
|
||||
@@ -37,7 +37,7 @@ async function main() {
|
||||
|
||||
// Log an asset first so the one-third cap isn't zero (a fresh family with
|
||||
// no assets has cap=0, which would make any bequest below "exceed" it).
|
||||
await page.locator('nav button[aria-label="Assets"]').click();
|
||||
await gotoTab(page, 'Assets');
|
||||
await page.waitForTimeout(500);
|
||||
await page.locator('.form-card .field:has-text("Description") input').fill('Savings');
|
||||
await page.locator('.form-card .field:has-text("Estimated value") input').fill('30000');
|
||||
@@ -45,7 +45,7 @@ async function main() {
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// ── Wassiyah: jurisdiction dropdown includes Singapore, defaults from family setting ──
|
||||
await page.locator('nav button[aria-label="Wassiyah"]').click();
|
||||
await gotoTab(page, 'Wassiyah');
|
||||
await page.waitForTimeout(800);
|
||||
const wassiyahJurisdiction = await page.locator('.field:has-text("Jurisdiction") select').inputValue();
|
||||
record('Wassiyah: jurisdiction defaults to the family setting (SG)', wassiyahJurisdiction === 'SG', wassiyahJurisdiction);
|
||||
@@ -86,7 +86,7 @@ async function main() {
|
||||
record('Wassiyah: marking reviewed records the reviewer name', reviewedVisible);
|
||||
|
||||
// Review status surfaces on the Mutawalli dashboard (owner-only family — owner sees own row)
|
||||
await page.locator('nav button[aria-label="Mutawalli"]').click();
|
||||
await gotoTab(page, 'Mutawalli');
|
||||
await page.waitForTimeout(800);
|
||||
const mutawalliReviewBadge = await page.locator('.review-badge.review-reviewed').isVisible().catch(() => false);
|
||||
const mutawalliGated = (await page.locator('.module').innerText()).includes('Only the mutawalli');
|
||||
@@ -95,7 +95,7 @@ async function main() {
|
||||
// ── Isolation: a different family (same account) is unaffected by this jurisdiction change ──
|
||||
const isolationPage = await browser.newPage({ viewport: { width: 390, height: 844 } });
|
||||
await signInFreshFamily(isolationPage, BASE, 'e2e-jurisdiction-isolation');
|
||||
await isolationPage.locator('nav button[aria-label="Settings"]').click();
|
||||
await gotoTab(isolationPage, 'Settings');
|
||||
await isolationPage.waitForTimeout(600);
|
||||
const isolatedJurisdiction = await isolationPage.locator('.jurisdiction-setting select').inputValue();
|
||||
record('Isolation: a different family defaults to MY, unaffected by the SG change above', isolatedJurisdiction === 'MY', isolatedJurisdiction);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// backend. Geolocation-dependent tabs use Playwright's mocked geolocation
|
||||
// (Kuala Lumpur coordinates) rather than the real device.
|
||||
const { chromium } = require('playwright');
|
||||
const { signInFreshFamily } = require('./e2e-auth-helper.cjs');
|
||||
const { signInFreshFamily, gotoTab } = require('./e2e-auth-helper.cjs');
|
||||
const BASE = 'https://moslem04.falahos.my/';
|
||||
const results = [];
|
||||
const consoleErrors = [];
|
||||
@@ -23,7 +23,7 @@ async function main() {
|
||||
await signInFreshFamily(page, BASE, 'e2e-khairat');
|
||||
|
||||
// ── Khairat ──
|
||||
await page.locator('nav button[aria-label="Khairat"]').click();
|
||||
await gotoTab(page, 'Khairat');
|
||||
await page.waitForTimeout(600);
|
||||
await page.locator('.field:has-text("Scheme name") input').fill('Kariah Khairat Kematian Masjid Al-Falah');
|
||||
await page.locator('.field:has-text("Organization") input').fill('Masjid Al-Falah');
|
||||
@@ -39,7 +39,7 @@ async function main() {
|
||||
record('Khairat: emergency fund progress bar computes correctly (1250/5000 = 25%)', fundProgressVisible);
|
||||
|
||||
// ── Trusted contacts with category + email ──
|
||||
await page.locator('nav button[aria-label="Assets"]').click();
|
||||
await gotoTab(page, 'Assets');
|
||||
await page.waitForTimeout(500);
|
||||
await page.locator('.contacts-section .field:has-text("Name") input').fill('Masjid Al-Falah Office');
|
||||
await page.locator('.contacts-section .field:has-text("Category") select').selectOption('mosque');
|
||||
@@ -50,7 +50,7 @@ async function main() {
|
||||
record('Trusted Contacts: category badge shows on the new contact', contactCategoryVisible);
|
||||
|
||||
// ── Qibla ──
|
||||
await page.locator('nav button[aria-label="Qibla"]').click();
|
||||
await gotoTab(page, 'Qibla');
|
||||
await page.waitForTimeout(500);
|
||||
await page.locator('button.btn-primary', { hasText: 'Find Qibla direction' }).click();
|
||||
await page.waitForTimeout(2000);
|
||||
@@ -61,7 +61,7 @@ async function main() {
|
||||
record('Qibla: shows distance to Makkah', distanceVisible);
|
||||
|
||||
// ── Prayer Times ──
|
||||
await page.locator('nav button[aria-label="Prayer Times"]').click();
|
||||
await gotoTab(page, 'Prayer Times');
|
||||
await page.waitForTimeout(500);
|
||||
await page.locator('button.btn-primary', { hasText: "Calculate today's prayer times" }).click();
|
||||
await page.waitForTimeout(2000);
|
||||
@@ -70,7 +70,7 @@ async function main() {
|
||||
record('Prayer Times: computes all 6 times for Kuala Lumpur', fajrVisible && /Fajr[\s\S]*Sunrise[\s\S]*Dhuhr[\s\S]*Asr[\s\S]*Maghrib[\s\S]*Isha/.test(timesInOrder), timesInOrder.replace(/\n/g, ' '));
|
||||
|
||||
// ── Locate (mosque search via live OpenStreetMap Overpass) ──
|
||||
await page.locator('nav button[aria-label="Locate"]').click();
|
||||
await gotoTab(page, 'Locate');
|
||||
await page.waitForTimeout(500);
|
||||
await page.locator('button.btn-primary', { hasText: 'Search nearby' }).click();
|
||||
await page.waitForTimeout(8000); // live Overpass API call
|
||||
@@ -78,7 +78,7 @@ async function main() {
|
||||
record('Locate: mosque search completes (real results or honest empty state)', locateResultOrEmpty);
|
||||
|
||||
// ── Quran ──
|
||||
await page.locator('nav button[aria-label="Quran"]').click();
|
||||
await gotoTab(page, 'Quran');
|
||||
await page.waitForTimeout(2000);
|
||||
const surahListVisible = await page.locator('.surah-row', { hasText: 'Al-Faatiha' }).isVisible().catch(() => false);
|
||||
record('Quran: Surah list loads from the public API (Al-Faatiha present)', surahListVisible);
|
||||
@@ -95,7 +95,7 @@ async function main() {
|
||||
// Neighbourhoods are scoped to the user account, not the family — the
|
||||
// shared e2e owner account may already belong to one from a prior run,
|
||||
// in which case the create/join form is behind the "add another" toggle.
|
||||
await page.locator('nav button[aria-label="Neighbourhood"]').click();
|
||||
await gotoTab(page, 'Neighbourhood');
|
||||
await page.waitForTimeout(600);
|
||||
const addToggleVisible = await page.locator('.add-toggle').isVisible().catch(() => false);
|
||||
if (addToggleVisible) await page.locator('.add-toggle').click();
|
||||
@@ -124,7 +124,7 @@ async function main() {
|
||||
const secondContext = await browser.newContext({ viewport: { width: 390, height: 844 } });
|
||||
const secondPage = await secondContext.newPage();
|
||||
await signInFreshFamily(secondPage, BASE, 'e2e-khairat-neighbour2');
|
||||
await secondPage.locator('nav button[aria-label="Neighbourhood"]').click();
|
||||
await gotoTab(secondPage, 'Neighbourhood');
|
||||
await secondPage.waitForTimeout(600);
|
||||
const secondAddToggleVisible = await secondPage.locator('.add-toggle').isVisible().catch(() => false);
|
||||
if (secondAddToggleVisible) await secondPage.locator('.add-toggle').click();
|
||||
|
||||
+2
-2
@@ -2,7 +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 { signInFreshFamily, gotoTab } = require('./e2e-auth-helper.cjs');
|
||||
const BASE = 'https://moslem04.falahos.my/';
|
||||
const results = [];
|
||||
const consoleErrors = [];
|
||||
@@ -15,7 +15,7 @@ async function main() {
|
||||
page.on('pageerror', e => consoleErrors.push(e.message));
|
||||
|
||||
await signInFreshFamily(page, BASE, 'e2e-other');
|
||||
const clickTab = async label => { await page.locator('nav button.tab', { hasText: label }).click(); await page.waitForTimeout(500); };
|
||||
const clickTab = async label => { await gotoTab(page, label); await page.waitForTimeout(500); };
|
||||
const selectByText = async (locator, text) => {
|
||||
// Options load async from Supabase now — poll until the matching one shows up.
|
||||
let val;
|
||||
|
||||
+8
-7
@@ -5,6 +5,7 @@
|
||||
// own trigger; the owner's own separate Wassiyah is not visible to the
|
||||
// member as "theirs" (proves per-author isolation, not just per-family).
|
||||
const { chromium } = require('playwright');
|
||||
const { gotoTab } = require('./e2e-auth-helper.cjs');
|
||||
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 : ''}`); }
|
||||
@@ -34,7 +35,7 @@ async function main() {
|
||||
await ownerPage.locator('button.btn-primary', { hasText: 'Create family' }).click();
|
||||
await ownerPage.waitForTimeout(1200);
|
||||
|
||||
await ownerPage.locator('nav button[aria-label="Family"]').click();
|
||||
await gotoTab(ownerPage, 'Manage');
|
||||
await ownerPage.waitForTimeout(500);
|
||||
await ownerPage.locator('.field:has-text("Invite by email") input').fill(MEMBER);
|
||||
await ownerPage.locator('.field:has-text("Role") select').selectOption('member');
|
||||
@@ -47,7 +48,7 @@ async function main() {
|
||||
record('Owner: invites both a member and an agent', bothInvited);
|
||||
|
||||
// Owner writes their OWN wassiyah bequest (should stay private to owner)
|
||||
await ownerPage.locator('nav button[aria-label="Wassiyah"]').click();
|
||||
await gotoTab(ownerPage, 'Wassiyah');
|
||||
await ownerPage.waitForTimeout(600);
|
||||
await ownerPage.locator('.form-card .field:has-text("Recipient name") input').fill('Owner Charity');
|
||||
await ownerPage.locator('.form-card .field:has-text("Relation to you") input').fill('charity');
|
||||
@@ -64,9 +65,9 @@ async function main() {
|
||||
const inviteVisible = await inviteRow.waitFor({ state: 'visible', timeout: 10000 }).then(() => true).catch(() => false);
|
||||
record('Member: sees pending invite', inviteVisible);
|
||||
await inviteRow.locator('.btn-small', { hasText: 'Accept' }).click();
|
||||
await memberPage.locator('nav button[aria-label="Coverage"]').waitFor({ state: 'visible', timeout: 10000 });
|
||||
await memberPage.locator('.hub-tab[aria-label="Home"]').waitFor({ state: 'visible', timeout: 10000 });
|
||||
|
||||
await memberPage.locator('nav button[aria-label="Wassiyah"]').click();
|
||||
await gotoTab(memberPage, 'Wassiyah');
|
||||
await memberPage.waitForTimeout(600);
|
||||
const ownerBequestVisibleToMember = await memberPage.locator('.bequest-row', { hasText: 'Owner Charity' }).isVisible().catch(() => false);
|
||||
record('Member: does NOT see owner\'s private bequest (per-author isolation)', !ownerBequestVisibleToMember);
|
||||
@@ -88,14 +89,14 @@ async function main() {
|
||||
const agentInviteVisible = await agentInviteRow.waitFor({ state: 'visible', timeout: 10000 }).then(() => true).catch(() => false);
|
||||
if (agentInviteVisible) {
|
||||
await agentInviteRow.locator('.btn-small', { hasText: 'Accept' }).click();
|
||||
await agentPage.locator('nav button[aria-label="Coverage"]').waitFor({ state: 'visible', timeout: 10000 });
|
||||
await agentPage.locator('.hub-tab[aria-label="Home"]').waitFor({ state: 'visible', timeout: 10000 });
|
||||
} else {
|
||||
// Agent may already belong to many families from prior test runs — switch to this one via Family tab
|
||||
await agentPage.locator('nav button[aria-label="Family"]').click().catch(() => {});
|
||||
await gotoTab(agentPage, 'Manage').catch(() => {});
|
||||
}
|
||||
record('Agent: accepts mutawalli invite', agentInviteVisible);
|
||||
|
||||
await agentPage.locator('nav button[aria-label="Mutawalli"]').click();
|
||||
await gotoTab(agentPage, 'Mutawalli');
|
||||
await agentPage.waitForTimeout(800);
|
||||
const memberChipVisible = await agentPage.locator('.member-chip', { hasText: MEMBER }).isVisible().catch(() => false);
|
||||
record('Mutawalli dashboard: shows the member in the chip list', memberChipVisible);
|
||||
|
||||
+2
-2
@@ -1,7 +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 { signInFreshFamily, gotoTab } = require('./e2e-auth-helper.cjs');
|
||||
const BASE = 'https://moslem04.falahos.my/';
|
||||
const results = [];
|
||||
const consoleErrors = [];
|
||||
@@ -14,7 +14,7 @@ async function main() {
|
||||
page.on('pageerror', e => consoleErrors.push(e.message));
|
||||
|
||||
await signInFreshFamily(page, BASE, 'e2e-property');
|
||||
const clickTab = async label => { await page.locator('nav button.tab', { hasText: label }).click(); await page.waitForTimeout(500); };
|
||||
const clickTab = async label => { await gotoTab(page, label); await page.waitForTimeout(500); };
|
||||
const selectByText = async (locator, text) => {
|
||||
// Options load async from Supabase now — poll until the matching one shows up.
|
||||
let val;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// privacy boundary), the Family Tree memorial-giving link, and the tree
|
||||
// completeness hints feeding the Coverage Dashboard's recommendations engine.
|
||||
const { chromium } = require('playwright');
|
||||
const { signInFreshFamily } = require('./e2e-auth-helper.cjs');
|
||||
const { signInFreshFamily, gotoTab } = require('./e2e-auth-helper.cjs');
|
||||
const BASE = 'https://moslem04.falahos.my/';
|
||||
const results = [];
|
||||
const consoleErrors = [];
|
||||
@@ -17,7 +17,7 @@ async function main() {
|
||||
await signInFreshFamily(page, BASE, 'e2e-sadaqah');
|
||||
|
||||
// ── Sadaqah tracker ──
|
||||
await page.locator('nav button[aria-label="Sadaqah"]').click();
|
||||
await gotoTab(page, 'Sadaqah');
|
||||
await page.waitForTimeout(600);
|
||||
|
||||
const zeroStreak = await page.locator('.streak-number').textContent();
|
||||
@@ -39,7 +39,7 @@ async function main() {
|
||||
record('Sadaqah: entry appears in own history with amount', historyRowVisible);
|
||||
|
||||
// ── Family Tree: add a deceased person, verify memorial link + dedication ──
|
||||
await page.locator('nav button[aria-label="Tree"]').click();
|
||||
await gotoTab(page, 'Tree');
|
||||
await page.waitForTimeout(600);
|
||||
await page.locator('.form-card .field:has-text("Full name") input').fill('Grandfather Yusuf');
|
||||
await page.locator('.form-card .field:has-text("Death date") input').fill('2010-05-01');
|
||||
@@ -53,7 +53,8 @@ async function main() {
|
||||
|
||||
await page.locator('.person-card', { hasText: 'Grandfather Yusuf' }).locator('button', { hasText: 'Give sadaqah in memory of' }).click();
|
||||
await page.waitForTimeout(800);
|
||||
const jumpedToSadaqah = await page.locator('nav button.tab.active[aria-label="Sadaqah"]').isVisible().catch(() => false);
|
||||
const jumpedToSadaqah = await page.locator('.hub-tab.active[aria-label="Giving"]').isVisible().catch(() => false)
|
||||
&& await page.locator('.subnav .tab.active[aria-label="Sadaqah"]').isVisible().catch(() => false);
|
||||
record('Tree: memorial link navigates to the Sadaqah tab', jumpedToSadaqah);
|
||||
|
||||
// Log a dedication and confirm the memorial count appears back on the Tree
|
||||
@@ -62,7 +63,7 @@ async function main() {
|
||||
await page.locator('button.btn-primary', { hasText: "Log today's sadaqah" }).click();
|
||||
await page.waitForTimeout(1200);
|
||||
|
||||
await page.locator('nav button[aria-label="Tree"]').click();
|
||||
await gotoTab(page, 'Tree');
|
||||
await page.waitForTimeout(600);
|
||||
await page.locator('.person-summary', { hasText: 'Grandfather Yusuf' }).click();
|
||||
await page.waitForTimeout(600);
|
||||
@@ -75,7 +76,7 @@ async function main() {
|
||||
await page.locator('.form-card button.btn-primary', { hasText: 'Add person' }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.locator('nav button[aria-label="Coverage"]').click();
|
||||
await gotoTab(page, 'Coverage');
|
||||
await page.waitForTimeout(800);
|
||||
const birthDateHint = await page.locator('.rec-row', { hasText: 'missing a birth date' }).isVisible().catch(() => false);
|
||||
record('Coverage: recommendations flag people missing a birth date', birthDateHint);
|
||||
@@ -85,7 +86,7 @@ async function main() {
|
||||
// ── Isolation: a different family sees none of this ──
|
||||
const isolationPage = await browser.newPage({ viewport: { width: 390, height: 844 } });
|
||||
await signInFreshFamily(isolationPage, BASE, 'e2e-sadaqah-isolation');
|
||||
await isolationPage.locator('nav button[aria-label="Sadaqah"]').click();
|
||||
await gotoTab(isolationPage, 'Sadaqah');
|
||||
await isolationPage.waitForTimeout(600);
|
||||
const isolatedStreak = await isolationPage.locator('.streak-number').textContent();
|
||||
record('Isolation: a different family starts with a 0 streak, unaffected by the above', isolatedStreak.trim() === '0', isolatedStreak);
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
// assume an anonymous landing page and need a sign-in prelude added before
|
||||
// they're valid again — tracked as a follow-up, not done here.
|
||||
const { chromium } = require('playwright');
|
||||
const { gotoTab } = require('./e2e-auth-helper.cjs');
|
||||
const BASE = 'https://moslem04.falahos.my/';
|
||||
const results = [];
|
||||
const consoleErrors = [];
|
||||
function record(name, pass, detail = '') { results.push({ name, pass, detail }); console.log(`${pass ? 'PASS' : 'FAIL'} ${name}${detail ? ' — ' + detail : ''}`); }
|
||||
|
||||
const TABS = ['Coverage', 'Faraid', 'Assets', 'Wassiyah', 'Hibah', 'Family Waqf', 'Nominate', 'Trigger', 'Claims (H2)', 'Family', 'Settings'];
|
||||
const TABS = ['Coverage', 'Faraid', 'Assets', 'Insurance', 'Zakat', 'Sadaqah', 'Khairat', 'Wassiyah', 'Hibah', 'Family Waqf', 'Nominate', 'Trigger', 'Mutawalli', 'Tree', 'Prayer Times', 'Qibla', 'Locate', 'Quran', 'Neighbourhood', 'Claims (H2)', 'Manage', 'Settings'];
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch();
|
||||
@@ -31,11 +32,11 @@ async function main() {
|
||||
await page.locator('.family-row').first().click();
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
const onMainApp = await page.locator('nav button[aria-label="Coverage"]').isVisible().catch(() => false);
|
||||
const onMainApp = await page.locator('.hub-tab[aria-label="Home"]').isVisible().catch(() => false);
|
||||
record('Signed-in owner reaches the main app', onMainApp);
|
||||
|
||||
for (const label of TABS) {
|
||||
await page.locator(`nav button[aria-label="${label}"]`).click();
|
||||
await gotoTab(page, label);
|
||||
await page.waitForTimeout(500);
|
||||
const infoBtn = page.locator('.module-header .info-btn');
|
||||
const hasInfoBtn = await infoBtn.isVisible().catch(() => false);
|
||||
|
||||
+2
-2
@@ -1,6 +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 { signInFreshFamily, gotoTab } = require('./e2e-auth-helper.cjs');
|
||||
const BASE = 'https://moslem04.falahos.my/';
|
||||
const results = [];
|
||||
const consoleErrors = [];
|
||||
@@ -13,7 +13,7 @@ async function main() {
|
||||
page.on('pageerror', e => consoleErrors.push(e.message));
|
||||
|
||||
await signInFreshFamily(page, BASE, 'e2e-trust');
|
||||
const clickTab = async label => { await page.locator('nav button.tab', { hasText: label }).click(); await page.waitForTimeout(500); };
|
||||
const clickTab = async label => { await gotoTab(page, label); await page.waitForTimeout(500); };
|
||||
|
||||
// Add a Property-type asset (land parcel)
|
||||
await clickTab('Assets');
|
||||
|
||||
+14
-13
@@ -1,7 +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 { signInFreshFamily, gotoTab } = require('./e2e-auth-helper.cjs');
|
||||
|
||||
const BASE = 'https://moslem04.falahos.my/';
|
||||
const results = [];
|
||||
@@ -25,15 +25,16 @@ async function main() {
|
||||
|
||||
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 gotoTab(page, label);
|
||||
await page.waitForTimeout(500);
|
||||
const isActive = await tabBtn.evaluate(el => el.classList.contains('active'));
|
||||
const isActive = label === 'Settings'
|
||||
? await page.locator('.settings-btn').evaluate(el => el.classList.contains('active'))
|
||||
: await page.locator('.subnav .tab', { hasText: label }).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 gotoTab(page, 'Faraid');
|
||||
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');
|
||||
@@ -48,7 +49,7 @@ async function main() {
|
||||
record('Faraid: wife+daughter+father+mother produces textbook shares', hasWife && hasDaughter && hasMother && hasFather, shareRows.join(' | '));
|
||||
|
||||
// ── Asset Registry: add an asset ──
|
||||
await page.locator('nav button.tab', { hasText: 'Assets' }).click();
|
||||
await gotoTab(page, 'Assets');
|
||||
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');
|
||||
@@ -70,7 +71,7 @@ async function main() {
|
||||
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 gotoTab(page, 'Wassiyah');
|
||||
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);
|
||||
@@ -110,7 +111,7 @@ async function main() {
|
||||
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 gotoTab(page, 'Hibah');
|
||||
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');
|
||||
@@ -127,7 +128,7 @@ async function main() {
|
||||
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 gotoTab(page, 'Family Waqf');
|
||||
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);
|
||||
@@ -145,7 +146,7 @@ async function main() {
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// ── Horizon 2 Claims: pre-pilot banner + issue + transfer restriction ──
|
||||
await page.locator('nav button.tab', { hasText: 'Claims (H2)' }).click();
|
||||
await gotoTab(page, 'Claims (H2)');
|
||||
await page.waitForTimeout(500);
|
||||
const pilotBannerVisible = await page.locator('.pilot-banner').isVisible();
|
||||
const bannerText = await page.locator('.pilot-banner').textContent();
|
||||
@@ -171,7 +172,7 @@ async function main() {
|
||||
record('Claims: transfer-within-pool updates status', statusAfterTransfer.trim() === 'transferred', statusAfterTransfer);
|
||||
|
||||
// ── Settings: export + delete-all guarded by confirm() ──
|
||||
await page.locator('nav button.tab', { hasText: 'Settings' }).click();
|
||||
await gotoTab(page, 'Settings');
|
||||
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();
|
||||
@@ -188,13 +189,13 @@ async function main() {
|
||||
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 local device data' }).click();
|
||||
await page.waitForTimeout(500);
|
||||
await page.locator('nav button.tab', { hasText: 'Assets' }).click();
|
||||
await gotoTab(page, 'Assets');
|
||||
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 gotoTab(page, 'Settings');
|
||||
await page.waitForTimeout(500);
|
||||
const taglineBefore = await page.locator('.header-tagline').textContent();
|
||||
await page.locator('.lang-btn', { hasText: 'Bahasa Malaysia' }).click();
|
||||
|
||||
+5
-5
@@ -2,7 +2,7 @@
|
||||
// correct 2.5% due once above Nisab, values autosave and persist across a
|
||||
// remount, and a fresh family (same account) doesn't see this member's figures.
|
||||
const { chromium } = require('playwright');
|
||||
const { signInFreshFamily } = require('./e2e-auth-helper.cjs');
|
||||
const { signInFreshFamily, gotoTab } = require('./e2e-auth-helper.cjs');
|
||||
const BASE = 'https://moslem04.falahos.my/';
|
||||
const results = [];
|
||||
const consoleErrors = [];
|
||||
@@ -15,7 +15,7 @@ async function main() {
|
||||
page.on('pageerror', e => consoleErrors.push(e.message));
|
||||
|
||||
await signInFreshFamily(page, BASE, 'e2e-zakat');
|
||||
await page.locator('nav button[aria-label="Zakat"]').click();
|
||||
await gotoTab(page, 'Zakat');
|
||||
await page.waitForTimeout(600);
|
||||
|
||||
// Below nisab: default nisab is 24000, wealth 10000 -> no zakat due
|
||||
@@ -41,9 +41,9 @@ async function main() {
|
||||
record('Zakat: deductible liabilities reduce zakatable wealth', wealthAfterDebt.trim() === '35,000', wealthAfterDebt);
|
||||
|
||||
// Persistence: reload the tab (remount) and confirm autosave held
|
||||
await page.locator('nav button[aria-label="Coverage"]').click();
|
||||
await gotoTab(page, 'Coverage');
|
||||
await page.waitForTimeout(400);
|
||||
await page.locator('nav button[aria-label="Zakat"]').click();
|
||||
await gotoTab(page, 'Zakat');
|
||||
await page.waitForTimeout(800);
|
||||
const cashPersisted = await page.locator('.field:has-text("Cash") input').inputValue();
|
||||
record('Zakat: autosaved figures persist across a tab remount', cashPersisted === '30000', cashPersisted);
|
||||
@@ -51,7 +51,7 @@ async function main() {
|
||||
// Isolation: a different family (same account) starts with defaults, not this member's figures
|
||||
const isolationPage = await browser.newPage({ viewport: { width: 390, height: 844 } });
|
||||
await signInFreshFamily(isolationPage, BASE, 'e2e-zakat-isolation');
|
||||
await isolationPage.locator('nav button[aria-label="Zakat"]').click();
|
||||
await gotoTab(isolationPage, 'Zakat');
|
||||
await isolationPage.waitForTimeout(600);
|
||||
const isolatedCash = await isolationPage.locator('.field:has-text("Cash") input').inputValue();
|
||||
record('Isolation: a different family does not see this member\'s Zakat figures', isolatedCash === '', isolatedCash);
|
||||
|
||||
+105
-36
@@ -52,19 +52,83 @@
|
||||
}
|
||||
});
|
||||
|
||||
const tabs = ['Coverage', 'Faraid', 'Assets', 'Insurance', 'Zakat', 'Sadaqah', 'Khairat', 'Wassiyah', 'Hibah', 'Family Waqf', 'Nominate', 'Trigger', 'Mutawalli', 'Tree', 'Prayer Times', 'Qibla', 'Locate', 'Quran', 'Neighbourhood', 'Claims (H2)', 'Family', 'Settings'];
|
||||
const icons = ['🎯', '📊', '📁', '🛡️', '🌙', '🤲', '🆘', '📜', '🎁', '⛲', '📇', '⚡', '🕋', '🌳', '🕌', '🧭', '📍', '📖', '📢', '🔗', '👥', '⚙️'];
|
||||
let activeTab = $state(0);
|
||||
// Five hubs instead of one 22-item flat tab strip — grouped by what the
|
||||
// user is actually trying to do (plan the estate, give, manage the
|
||||
// family, or use it day-to-day), not alphabetically or by build order.
|
||||
// Research consistently points to 4-5 primary destinations as the
|
||||
// ceiling for a bottom nav; everything else lives one tap deeper inside
|
||||
// its hub instead of competing for space in a single scrolling row.
|
||||
const HUBS = [
|
||||
{ key: 'home', label: 'Home', icon: '🎯', tabs: [
|
||||
{ label: 'Coverage', icon: '🎯', component: CoverageDashboard }
|
||||
] },
|
||||
{ key: 'estate', label: 'Estate', icon: '📁', tabs: [
|
||||
{ label: 'Assets', icon: '📁', component: AssetRegistry },
|
||||
{ label: 'Faraid', icon: '📊', component: FaraidCalculator },
|
||||
{ label: 'Insurance', icon: '🛡️', component: InsurancePolicies },
|
||||
{ label: 'Wassiyah', icon: '📜', component: WassiyahGenerator },
|
||||
{ label: 'Hibah', icon: '🎁', component: HibahTracker },
|
||||
{ label: 'Family Waqf', icon: '⛲', component: FamilyWaqfDesignator },
|
||||
{ label: 'Nominate', icon: '📇', component: NominationRegistry },
|
||||
{ label: 'Claims (H2)', icon: '🔗', component: DigitalClaims }
|
||||
] },
|
||||
{ key: 'giving', label: 'Giving', icon: '🤲', tabs: [
|
||||
{ label: 'Zakat', icon: '🌙', component: ZakatCalculator },
|
||||
{ label: 'Sadaqah', icon: '🤲', component: SadaqahTracker },
|
||||
{ label: 'Khairat', icon: '🆘', component: KhairatTracker }
|
||||
] },
|
||||
{ key: 'family', label: 'Family', icon: '👪', tabs: [
|
||||
{ label: 'Tree', icon: '🌳', component: FamilyTree },
|
||||
{ label: 'Trigger', icon: '⚡', component: DeathTrigger },
|
||||
{ label: 'Mutawalli', icon: '🕋', component: MutawalliDashboard },
|
||||
{ label: 'Manage', icon: '👥', component: FamilyManagement },
|
||||
{ label: 'Neighbourhood', icon: '📢', component: NeighbourhoodBoard }
|
||||
] },
|
||||
{ key: 'daily', label: 'Daily', icon: '🕌', tabs: [
|
||||
{ label: 'Prayer Times', icon: '🕌', component: PrayerTimes },
|
||||
{ label: 'Qibla', icon: '🧭', component: QiblaFinder },
|
||||
{ label: 'Quran', icon: '📖', component: QuranReader },
|
||||
{ label: 'Locate', icon: '📍', component: Locators }
|
||||
] }
|
||||
];
|
||||
|
||||
let activeHubKey = $state('home');
|
||||
// Remembers which sub-tab was last open in each hub, so switching hubs
|
||||
// and back doesn't reset your place.
|
||||
let subIndexByHub = $state(Object.fromEntries(HUBS.map(h => [h.key, 0])));
|
||||
let showSettings = $state(false);
|
||||
|
||||
const activeHub = $derived(HUBS.find(h => h.key === activeHubKey));
|
||||
const activeSubTab = $derived(activeHub.tabs[subIndexByHub[activeHubKey]] ?? activeHub.tabs[0]);
|
||||
const CurrentComponent = $derived(activeSubTab.component);
|
||||
|
||||
function selectHub(key) {
|
||||
activeHubKey = key;
|
||||
showSettings = false;
|
||||
}
|
||||
function selectSubTab(index) {
|
||||
subIndexByHub = { ...subIndexByHub, [activeHubKey]: index };
|
||||
showSettings = false;
|
||||
}
|
||||
|
||||
// Cross-component navigation (e.g. the Family Tree's "give sadaqah in
|
||||
// memory of" link) requests a tab by label — resolve it to whichever
|
||||
// hub actually contains that label.
|
||||
requestedTab.subscribe(name => {
|
||||
if (!name) return;
|
||||
const idx = tabs.indexOf(name);
|
||||
if (idx >= 0) activeTab = idx;
|
||||
for (const hub of HUBS) {
|
||||
const idx = hub.tabs.findIndex(t => t.label === name);
|
||||
if (idx >= 0) { activeHubKey = hub.key; subIndexByHub = { ...subIndexByHub, [hub.key]: idx }; showSettings = false; break; }
|
||||
}
|
||||
requestedTab.set(null);
|
||||
});
|
||||
|
||||
function handleKeydown(e) {
|
||||
if (e.key === 'ArrowRight') activeTab = (activeTab + 1) % tabs.length;
|
||||
if (e.key === 'ArrowLeft') activeTab = (activeTab - 1 + tabs.length) % tabs.length;
|
||||
const tabs = activeHub.tabs;
|
||||
if (tabs.length < 2) return;
|
||||
const current = subIndexByHub[activeHubKey] ?? 0;
|
||||
if (e.key === 'ArrowRight') selectSubTab((current + 1) % tabs.length);
|
||||
if (e.key === 'ArrowLeft') selectSubTab((current - 1 + tabs.length) % tabs.length);
|
||||
}
|
||||
|
||||
function doExport() {
|
||||
@@ -95,6 +159,7 @@
|
||||
{:else}
|
||||
<div class="app">
|
||||
<header>
|
||||
<button class="settings-btn" class:active={showSettings} onclick={() => showSettings = !showSettings} aria-label="Settings">⚙️</button>
|
||||
<div class="header-brand">
|
||||
<div class="brand-line brand-line-first">Nur</div>
|
||||
<div class="brand-line brand-line-second">Falah</div>
|
||||
@@ -103,38 +168,19 @@
|
||||
<div class="header-divider"></div>
|
||||
</header>
|
||||
|
||||
<nav>
|
||||
{#each tabs as tab, i}
|
||||
<button class="tab" class:active={activeTab === i} onclick={() => activeTab = i} aria-label={tab}>
|
||||
<span class="tab-icon">{icons[i]}</span>
|
||||
<span class="tab-label">{tab}</span>
|
||||
{#if !showSettings && activeHub.tabs.length > 1}
|
||||
<nav class="subnav">
|
||||
{#each activeHub.tabs as t, i}
|
||||
<button class="tab" class:active={subIndexByHub[activeHubKey] === i} onclick={() => selectSubTab(i)} aria-label={t.label}>
|
||||
<span class="tab-icon">{t.icon}</span>
|
||||
<span class="tab-label">{t.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</nav>
|
||||
{/if}
|
||||
|
||||
<main>
|
||||
{#if activeTab === 0}<CoverageDashboard />
|
||||
{:else if activeTab === 1}<FaraidCalculator />
|
||||
{:else if activeTab === 2}<AssetRegistry />
|
||||
{:else if activeTab === 3}<InsurancePolicies />
|
||||
{:else if activeTab === 4}<ZakatCalculator />
|
||||
{:else if activeTab === 5}<SadaqahTracker />
|
||||
{:else if activeTab === 6}<KhairatTracker />
|
||||
{:else if activeTab === 7}<WassiyahGenerator />
|
||||
{:else if activeTab === 8}<HibahTracker />
|
||||
{:else if activeTab === 9}<FamilyWaqfDesignator />
|
||||
{:else if activeTab === 10}<NominationRegistry />
|
||||
{:else if activeTab === 11}<DeathTrigger />
|
||||
{:else if activeTab === 12}<MutawalliDashboard />
|
||||
{:else if activeTab === 13}<FamilyTree />
|
||||
{:else if activeTab === 14}<PrayerTimes />
|
||||
{:else if activeTab === 15}<QiblaFinder />
|
||||
{:else if activeTab === 16}<Locators />
|
||||
{:else if activeTab === 17}<QuranReader />
|
||||
{:else if activeTab === 18}<NeighbourhoodBoard />
|
||||
{:else if activeTab === 19}<DigitalClaims />
|
||||
{:else if activeTab === 20}<FamilyManagement />
|
||||
{:else if activeTab === 21}
|
||||
{#if showSettings}
|
||||
<div class="module">
|
||||
<div class="module-header">
|
||||
<h2>Settings</h2>
|
||||
@@ -161,8 +207,19 @@
|
||||
<button class="btn-secondary" onclick={signOut}>Sign out</button>
|
||||
<button class="btn-danger" onclick={doDelete}>Delete local device data — irreversible</button>
|
||||
</div>
|
||||
{:else}
|
||||
<CurrentComponent />
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<nav class="hub-nav">
|
||||
{#each HUBS as hub}
|
||||
<button class="hub-tab" class:active={!showSettings && activeHubKey === hub.key} onclick={() => selectHub(hub.key)} aria-label={hub.label}>
|
||||
<span class="hub-icon">{hub.icon}</span>
|
||||
<span class="hub-label">{hub.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -187,9 +244,11 @@
|
||||
:global(#app) { position: relative; z-index: 2; }
|
||||
.loading-screen { display: flex; align-items: center; justify-content: center; min-height: 100dvh; color: #8A8478; font-size: 14px; }
|
||||
|
||||
.app { max-width: 480px; margin: 0 auto; min-height: 100dvh; display: flex; flex-direction: column; padding-bottom: 80px; }
|
||||
.app { max-width: 480px; margin: 0 auto; min-height: 100dvh; display: flex; flex-direction: column; padding-bottom: 88px; }
|
||||
|
||||
header { text-align: center; padding: 28px 16px 16px; background: rgba(12,17,23,0.95); backdrop-filter: blur(12px); border-bottom: 1px solid rgba(201,168,76,0.15); position: sticky; top: 0; z-index: 10; }
|
||||
.settings-btn { position: absolute; top: 20px; right: 14px; background: none; border: none; font-size: 18px; cursor: pointer; opacity: 0.7; padding: 6px; border-radius: 8px; }
|
||||
.settings-btn.active { opacity: 1; background: rgba(201,168,76,0.12); }
|
||||
.header-brand { display: flex; justify-content: center; gap: 8px; }
|
||||
.brand-line { font-family: 'DM Serif Display', serif; font-size: 26px; }
|
||||
.brand-line-first { color: #E8E4DC; }
|
||||
@@ -197,7 +256,9 @@
|
||||
.header-tagline { font-size: 10px; letter-spacing: 2px; color: #8A8478; }
|
||||
.header-divider { height: 2px; width: 40px; background: #C9A84C; margin: 10px auto 0; border-radius: 2px; }
|
||||
|
||||
nav { display: flex; overflow-x: auto; gap: 4px; padding: 10px 8px; background: rgba(12,17,23,0.7); border-bottom: 1px solid rgba(201,168,76,0.1); position: sticky; top: 92px; z-index: 9; }
|
||||
/* Sub-nav — this hub's own tabs, revealed one level below the primary
|
||||
bottom nav rather than competing with 21 other items for space. */
|
||||
.subnav { display: flex; overflow-x: auto; gap: 4px; padding: 10px 8px; background: rgba(12,17,23,0.7); border-bottom: 1px solid rgba(201,168,76,0.1); position: sticky; top: 92px; z-index: 9; }
|
||||
.tab { flex-shrink: 0; display: flex; flex-direction: column; align-items: center; gap: 3px; padding: 8px 12px; background: none; border: none; border-radius: 10px; cursor: pointer; color: #8A8478; }
|
||||
.tab.active { background: rgba(201,168,76,0.12); color: #C9A84C; }
|
||||
.tab-icon { font-size: 18px; }
|
||||
@@ -205,6 +266,14 @@
|
||||
|
||||
main { flex: 1; padding: 16px; }
|
||||
|
||||
/* Primary nav — 5 hubs, fixed to the bottom so every destination stays
|
||||
in thumb reach, matching the pattern every reference app converges on. */
|
||||
.hub-nav { display: flex; position: fixed; bottom: 0; left: 50%; transform: translateX(-50%); width: 100%; max-width: 480px; background: rgba(12,17,23,0.97); backdrop-filter: blur(12px); border-top: 1px solid rgba(201,168,76,0.15); z-index: 11; padding: 6px 4px calc(6px + env(safe-area-inset-bottom, 0px)); }
|
||||
.hub-tab { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 8px 2px; background: none; border: none; border-radius: 10px; cursor: pointer; color: #8A8478; }
|
||||
.hub-tab.active { color: #C9A84C; }
|
||||
.hub-icon { font-size: 20px; }
|
||||
.hub-label { font-size: 10px; }
|
||||
|
||||
:global(.btn-danger) { width: 100%; padding: 12px; border-radius: 8px; border: 1px solid rgba(239,68,68,0.4); background: rgba(239,68,68,0.1); color: #EF4444; font-weight: 600; cursor: pointer; margin-top: 12px; }
|
||||
:global(.btn-secondary) { width: 100%; padding: 12px; border-radius: 8px; border: none; background: rgba(255,255,255,0.08); color: #E8E4DC; font-weight: 600; cursor: pointer; }
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// checks the family switcher shows exactly one family, and inspects
|
||||
// Coverage/Assets/Tree for expected content.
|
||||
const { chromium } = require('playwright');
|
||||
const { gotoTab } = require('./e2e-auth-helper.cjs');
|
||||
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 : ''}`); }
|
||||
@@ -38,12 +39,12 @@ async function main() {
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
}
|
||||
const onMainApp = await page.locator('nav button[aria-label="Coverage"]').isVisible({ timeout: 8000 }).catch(() => false);
|
||||
const onMainApp = await page.locator('.hub-tab[aria-label="Home"]').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 gotoTab(page, 'Assets');
|
||||
await page.waitForTimeout(800);
|
||||
const assetCount = await page.locator('.asset-row').count();
|
||||
record(`${fam.name}: has ${fam.expectedAssets} assets`, assetCount === fam.expectedAssets, `found ${assetCount}`);
|
||||
@@ -51,13 +52,13 @@ async function main() {
|
||||
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 gotoTab(page, 'Coverage');
|
||||
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 gotoTab(page, 'Tree');
|
||||
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}`);
|
||||
|
||||
Reference in New Issue
Block a user