33a821e61d
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).
128 lines
7.6 KiB
JavaScript
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); });
|