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:
+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();
|
||||
|
||||
Reference in New Issue
Block a user