Files
wmj 33a821e61d 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).
2026-08-14 14:55:09 +08:00

128 lines
7.6 KiB
JavaScript

// Verifies the mini family genealogy map end-to-end against the live backend:
// add people (with photo upload to Supabase Storage), link parent/child and
// spouse relationships, confirm the generational tree renders correctly, and
// that a second family's tree is fully isolated (no data bleed across
// families sharing the same account, matching the isolation pattern already
// proven for Wassiyah/Waqf).
const { chromium } = require('playwright');
const path = require('path');
const { signInFreshFamily, 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 : ''}`); }
async function main() {
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 390, height: 844 } });
page.on('console', m => { if (m.type() === 'error') consoleErrors.push(m.text()); });
page.on('pageerror', e => consoleErrors.push(e.message));
const familyName = await signInFreshFamily(page, BASE, 'e2e-family-tree');
await gotoTab(page, 'Tree');
await page.waitForTimeout(600);
// Add three people: grandfather, father, son
async function addPerson(name, extra = {}) {
await page.locator('.form-card .field:has-text("Full name") input').fill(name);
if (extra.gender) await page.locator('.form-card .field:has-text("Gender") select').selectOption(extra.gender);
if (extra.birthDate) await page.locator('.form-card .field:has-text("Birth date") input').fill(extra.birthDate);
await page.locator('.form-card button.btn-primary', { hasText: 'Add person' }).click();
await page.locator('.person-card', { hasText: name }).waitFor({ state: 'visible', timeout: 10000 });
}
await addPerson('Grandfather Ahmad', { gender: 'male', birthDate: '1940-01-01' });
await addPerson('Father Ismail', { gender: 'male', birthDate: '1965-05-15' });
await addPerson('Grandmother Fatimah', { gender: 'female', birthDate: '1945-03-10' });
await addPerson('Son Yusuf', { gender: 'male', birthDate: '1995-08-20' });
const allAdded = await page.locator('.person-card').count();
record('Family Tree: 4 people added successfully', allAdded === 4, `${allAdded} cards`);
// Link relationships: Grandfather is parent of Father; Grandfather spouse_of Grandmother; Father parent of Son
async function addRelationship(personALabel, type, personBLabel) {
// The relationship form-card has exactly 3 selects in DOM order: person A,
// type, person B. Selecting by position avoids :has-text substring
// ambiguity (the type select's own option "is spouse of" contains
// "spouse", so a text-based locator for the third field also matches it).
const relCard = page.locator('.form-card', { has: page.locator('button.btn-primary', { hasText: 'Add relationship' }) });
const selects = relCard.locator('select');
await selects.nth(0).selectOption({ label: personALabel });
await selects.nth(1).selectOption(type);
await page.waitForTimeout(200);
await selects.nth(2).selectOption({ label: personBLabel });
await page.locator('button.btn-primary', { hasText: 'Add relationship' }).click();
await page.waitForTimeout(600);
}
await addRelationship('Grandfather Ahmad', 'parent_of', 'Father Ismail');
await addRelationship('Grandfather Ahmad', 'spouse_of', 'Grandmother Fatimah');
await addRelationship('Father Ismail', 'parent_of', 'Son Yusuf');
const relCount = await page.locator('.rel-row').count();
record('Family Tree: 3 relationships recorded', relCount === 3, `${relCount} rows`);
// Verify tree structure: Grandfather is a root, Father nested under him, Son nested under Father
const treeText = await page.locator('.tree-section').innerText();
const rootShown = treeText.includes('Grandfather Ahmad');
const spouseShown = treeText.includes('Grandmother Fatimah');
const fatherNested = treeText.includes('Father Ismail');
const sonNested = treeText.includes('Son Yusuf');
record('Family Tree: generational tree renders all 4 people correctly nested', rootShown && spouseShown && fatherNested && sonNested, treeText.replace(/\s+/g, ' ').slice(0, 300));
// Photo upload — real file, real Supabase Storage
await page.locator('.person-summary', { hasText: 'Father Ismail' }).click();
await page.waitForTimeout(400);
const fileInput = page.locator('.person-card', { hasText: 'Father Ismail' }).locator('input[type=file]');
await fileInput.setInputFiles(path.join(__dirname, 'test-photo.png'));
await page.waitForTimeout(2500);
const avatarImgVisible = await page.locator('.person-card', { hasText: 'Father Ismail' }).locator('img.avatar').isVisible().catch(() => false);
record('Family Tree: photo uploads and renders as the person\'s avatar', avatarImgVisible);
// Photo removal
if (avatarImgVisible) {
await page.locator('.person-card', { hasText: 'Father Ismail' }).locator('button', { hasText: 'Remove photo' }).click();
await page.waitForTimeout(1000);
const avatarGoneBack = await page.locator('.person-card', { hasText: 'Father Ismail' }).locator('.avatar-placeholder').isVisible().catch(() => false);
record('Family Tree: removing photo reverts to initial-letter placeholder', avatarGoneBack);
}
// Edit a person (expand the card first — Edit is only visible when expanded)
await page.locator('.person-summary', { hasText: 'Son Yusuf' }).click();
await page.waitForTimeout(300);
await page.locator('.person-card', { hasText: 'Son Yusuf' }).locator('button', { hasText: 'Edit' }).click();
await page.waitForTimeout(300);
await page.locator('.form-card .field:has-text("Notes") input').fill('Eldest grandchild');
await page.locator('.form-card button.btn-primary', { hasText: 'Save changes' }).click();
await page.waitForTimeout(600);
// Card should still be expanded (editing doesn't collapse it) — check directly.
const notesVisible = await page.locator('.person-card', { hasText: 'Son Yusuf' }).locator('.notes').isVisible().catch(() => false);
record('Family Tree: editing a person persists notes', notesVisible);
// Delete a relationship, confirm it's gone
const delBtn = page.locator('.rel-row', { hasText: 'Grandmother Fatimah' }).locator('button');
await delBtn.click();
await page.waitForTimeout(600);
const relCountAfterDelete = await page.locator('.rel-row').count();
record('Family Tree: deleting a relationship removes it', relCountAfterDelete === 2, `${relCountAfterDelete} rows remain`);
// ── 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 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);
await isolationPage.close();
record('No uncaught JS console errors during full session', consoleErrors.length === 0, consoleErrors.join(' || '));
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); });