# Instructions

- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.

# Test info

- Name: fundcustodian.spec.js >> Onboarding Tour (custodian-v27) >> SW cache version is current
- Location: tests/fundcustodian.spec.js:824:3

# Error details

```
Error: expect(locator).toContainText(expected) failed

Locator: locator('#my-balance')
Expected substring: "₹"
Timeout: 10000ms
Error: element(s) not found

Call log:
  - Expect "toContainText" with timeout 10000ms
  - waiting for locator('#my-balance')

```

```yaml
- text: 404 page not found
```

# Test source

```ts
  695 |     await page.locator('#mgmt-tab-payees').click();
  696 |     await expect(page.locator('#mgmt-payee-list .btn-edit').first()).toBeVisible({ timeout: 6000 });
  697 |     await page.locator('#mgmt-payee-list .btn-edit').first().click();
  698 |     await expect(page.locator('#edit-payee-modal')).toHaveClass(/open/);
  699 |     await expect(page.locator('#ep-name')).not.toHaveValue('');
  700 |     await page.locator('#edit-payee-modal .btn-s').click(); // Cancel
  701 |     await expect(page.locator('#edit-payee-modal')).not.toHaveClass(/open/);
  702 |   });
  703 | 
  704 |   test('Reconcile tab: validation requires narration', async ({ page }) => {
  705 |     await page.locator('#mgmt-tab-reconcile').click();
  706 |     await expect(page.locator('#mgmt-panel-reconcile')).toBeVisible();
  707 |     await page.locator('#recon-custodian').selectOption('gurpreet');
  708 |     await page.locator('#recon-amount').fill('1000');
  709 |     await page.locator('button:has-text("Record Write-Off")').click();
  710 |     await expect(page.locator('#toast')).toContainText('Narration');
  711 |   });
  712 | 
  713 |   test('Add Payee form requires a name', async ({ page }) => {
  714 |     await page.locator('#mgmt-tab-payees').click();
  715 |     await page.locator('button:has-text("Add Payee")').click();
  716 |     await expect(page.locator('#toast')).toContainText('name');
  717 |   });
  718 | });
  719 | 
  720 | // ─────────────────────────────────────────────────────────────────
  721 | // RECEIPT↔ADVANCE FIX (cash return settles the advance) — 2026-05-31
  722 | // ─────────────────────────────────────────────────────────────────
  723 | const DB_C = 'https://srv1111289.hstgr.cloud/db';
  724 | const C_HDR = { 'Content-Type':'application/json', 'Content-Profile':'custodian', 'Accept-Profile':'custodian' };
  725 | 
  726 | test.describe('Cash-return RPC guards (web_anon)', () => {
  727 |   test('settle_advance_by_cash is reachable and guards a missing advance', async ({ request }) => {
  728 |     const r = await request.post(`${DB_C}/rpc/settle_advance_by_cash`, {
  729 |       headers: C_HDR, data: { p_advance_id:'adv-NOPE', p_amount:100, p_by:'pramod' }
  730 |     });
  731 |     expect(r.ok()).toBeTruthy();              // 200 (DEFINER reachable, no 42501)
  732 |     const d = await r.json();
  733 |     expect(d.ok).toBe(false);
  734 |     expect(JSON.stringify(d)).not.toMatch(/permission denied|42501/i);
  735 |     expect(d.error).toMatch(/not found/i);
  736 |   });
  737 | 
  738 |   test('rejects a non-positive amount', async ({ request }) => {
  739 |     const r = await request.post(`${DB_C}/rpc/settle_advance_by_cash`, {
  740 |       headers: C_HDR, data: { p_advance_id:'adv-NOPE', p_amount:0, p_by:'pramod' }
  741 |     });
  742 |     const d = await r.json();
  743 |     expect(d.ok).toBe(false);
  744 |   });
  745 | });
  746 | 
  747 | test.describe('Cash-return UI (receiving + employee)', () => {
  748 |   test.beforeEach(async ({ page }) => {
  749 |     await injectSession(page, SESSION.pramod);
  750 |     await page.goto(BASE_URL);
  751 |     await handleModeSelector(page, 'custodian');
  752 |     await page.locator('#nav-transfer').click();
  753 |     await expect(page.locator('#s-transfer')).toHaveClass(/active/, { timeout: 4000 });
  754 |   });
  755 | 
  756 |   test('Receiving + Employee shows people search (chooser appears only with advances)', async ({ page }) => {
  757 |     await page.locator('#tf-cat-chips [data-cat="employee"]').click();
  758 |     await page.locator('#role-receiver').click();
  759 |     await expect(page.locator('#tf-people-section')).toBeVisible();
  760 |     // No payee selected yet → chooser hidden, general purpose field shown
  761 |     await expect(page.locator('#tf-recv-adv-section')).not.toBeVisible();
  762 |     await expect(page.locator('#tf-rcpt-purpose-group')).toBeVisible();
  763 |   });
  764 | 
  765 |   test('advance-type chips hidden when receiving (only for giving)', async ({ page }) => {
  766 |     await page.locator('#tf-cat-chips [data-cat="employee"]').click();
  767 |     await page.locator('#role-receiver').click();
  768 |     await expect(page.locator('#tf-advtype-section')).not.toBeVisible();
  769 |   });
  770 | });
  771 | 
  772 | test.describe('Ledger Account tab still ties to wallet (settlements now shown)', () => {
  773 |   test('account running balance renders for admin without error', async ({ page }) => {
  774 |     await injectSession(page, SESSION.pramod);
  775 |     await page.goto(BASE_URL);
  776 |     await handleModeSelector(page, 'custodian');
  777 |     await page.locator('#nav-ledger').click();
  778 |     await expect(page.locator('#s-ledger')).toHaveClass(/active/, { timeout: 4000 });
  779 |     await page.locator('#tab-account').click();
  780 |     await expect(page.locator('#ledger-account-panel')).toBeVisible();
  781 |     // Either rows or an empty-state — must not throw / stay on spinner
  782 |     await expect(page.locator('#account-list .acct-row, #account-list .empty')
  783 |       .first()).toBeVisible({ timeout: 8000 });
  784 |   });
  785 | });
  786 | 
  787 | // ─────────────────────────────────────────────────────────────────
  788 | // ONBOARDING TOUR — v27
  789 | // ─────────────────────────────────────────────────────────────────
  790 | test.describe('Onboarding Tour (custodian-v27)', () => {
  791 |   test.beforeEach(async ({ page }) => {
  792 |     // Use non-admin (shambhu) so dashboard loads directly without mode selector
  793 |     await injectSession(page, SESSION.shambhu);
  794 |     await page.goto(BASE_URL);
> 795 |     await expect(page.locator('#my-balance')).toContainText('₹', { timeout: 10000 });
      |                                               ^ Error: expect(locator).toContainText(expected) failed
  796 |   });
  797 | 
  798 |   test('? tour button present on dashboard', async ({ page }) => {
  799 |     await expect(page.locator('#btn-tour')).toBeVisible();
  800 |   });
  801 | 
  802 |   test('tour overlay opens and shows 13 steps', async ({ page }) => {
  803 |     await page.locator('#btn-tour').click();
  804 |     await expect(page.locator('#tour-card')).toBeVisible({ timeout: 3000 });
  805 |     await expect(page.locator('#t-step')).toHaveText('Step 1 of 13');
  806 |     await expect(page.locator('#t-title')).toHaveText('Welcome to Fund Custodian');
  807 |   });
  808 | 
  809 |   test('tour navigates forward and back', async ({ page }) => {
  810 |     await page.locator('#btn-tour').click();
  811 |     await page.locator('#t-next').click();
  812 |     await expect(page.locator('#t-step')).toHaveText('Step 2 of 13');
  813 |     await page.locator('#t-prev').click();
  814 |     await expect(page.locator('#t-step')).toHaveText('Step 1 of 13');
  815 |   });
  816 | 
  817 |   test('tour skip button closes overlay', async ({ page }) => {
  818 |     await page.locator('#btn-tour').click();
  819 |     await expect(page.locator('#tour-card')).toBeVisible();
  820 |     await page.locator('#t-skip').click();
  821 |     await expect(page.locator('#tour-card')).not.toBeVisible({ timeout: 2000 });
  822 |   });
  823 | 
  824 |   test('SW cache version is current', async ({ page }) => {
  825 |     const html = await page.content();
  826 |     expect(html).toMatch(/custodian-v\d+/);
  827 |   });
  828 | });
  829 | 
```