# 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: customers.spec.js >> Customer Management — HTML Structure >> 15 customer form fields present
- Location: tests/customers.spec.js:91:3

# Error details

```
Error: expect(received).toContain(expected) // indexOf

Expected substring: "id=\"f-name\""
Received string:    "404 page not found
"
```

# Test source

```ts
  1   | // customers.spec.js — Customer Management mini-PWA (Phases 1–5)
  2   | // Prereqs: test_data.sql applied so deterministic rows exist.
  3   | //          Cleanup via test_data_cleanup.sql is NOT run by this spec.
  4   | const { test, expect, devices, request } = require('@playwright/test');
  5   | 
  6   | const PWA_NAME = 'Customer Management';
  7   | const BASE_URL = 'https://srv1111289.hstgr.cloud/sales/customers/';
  8   | const DB       = 'https://srv1111289.hstgr.cloud/db';
  9   | 
  10  | const RUN_ID = `pw${Date.now().toString(36)}`;
  11  | 
  12  | test.use({ ...devices['Pixel 6'] });
  13  | 
  14  | // ── helpers ───────────────────────────────────────────────────
  15  | function injectSession(page, id = 'harish', name = 'Harish Kumar Lal') {
  16  |   return page.addInitScript(({ id, name }) => {
  17  |     localStorage.setItem('lm360-session', JSON.stringify({
  18  |       empId: id, name, role: 'admin', loginAt: Date.now()
  19  |     }));
  20  |   }, { id, name });
  21  | }
  22  | 
  23  | async function dbDelete(table, filter, profile = 'sales') {
  24  |   const ctx = await request.newContext();
  25  |   await ctx.fetch(`${DB}/${table}?${filter}`, {
  26  |     method: 'DELETE',
  27  |     headers: { 'Accept-Profile': profile, 'Content-Profile': profile }
  28  |   });
  29  |   await ctx.dispose();
  30  | }
  31  | 
  32  | // ──────────────────────────────────────────────────────────────
  33  | // SMOKE
  34  | // ──────────────────────────────────────────────────────────────
  35  | test.describe(`${PWA_NAME} — Smoke`, () => {
  36  |   test('page returns 200', async ({ page }) => {
  37  |     const resp = await page.goto(BASE_URL);
  38  |     expect(resp?.status()).toBe(200);
  39  |   });
  40  | 
  41  |   test('manifest.json returns 200', async ({ page }) => {
  42  |     const resp = await page.goto(BASE_URL + 'manifest.json');
  43  |     expect(resp?.status()).toBe(200);
  44  |   });
  45  | 
  46  |   test('HTML carries current customers SW version', async () => {
  47  |     const ctx = await request.newContext();
  48  |     const resp = await ctx.get(BASE_URL);
  49  |     const html = await resp.text();
  50  |     expect(html).toMatch(/customers-v\d+/);
  51  |     expect(html).toContain('Customer Management');
  52  |     await ctx.dispose();
  53  |   });
  54  | });
  55  | 
  56  | // ──────────────────────────────────────────────────────────────
  57  | // STRUCTURE — HTML carries all required elements
  58  | // ──────────────────────────────────────────────────────────────
  59  | test.describe(`${PWA_NAME} — HTML Structure`, () => {
  60  |   let html;
  61  |   test.beforeAll(async () => {
  62  |     const ctx = await request.newContext();
  63  |     html = await (await ctx.get(BASE_URL)).text();
  64  |     await ctx.dispose();
  65  |   });
  66  | 
  67  |   test('three core screens present', () => {
  68  |     expect(html).toContain('id="s-list"');
  69  |     expect(html).toContain('id="s-detail"');
  70  |     expect(html).toContain('id="s-form"');
  71  |     expect(html).toContain('id="s-pipeline"');
  72  |   });
  73  | 
  74  |   test('summary tiles present', () => {
  75  |     expect(html).toContain('id="t-total"');
  76  |     expect(html).toContain('id="t-business"');
  77  |     expect(html).toContain('id="t-individual"');
  78  |   });
  79  | 
  80  |   test('bottom nav has 3 tabs (List, Pipeline, Add)', () => {
  81  |     expect(html).toContain('id="nav-list"');
  82  |     expect(html).toContain('id="nav-pipeline"');
  83  |     expect(html).toContain('id="nav-add"');
  84  |   });
  85  | 
  86  |   test('7 detail tabs declared', () => {
  87  |     ['info','contacts','addresses','jobs','offers','deals','comms']
  88  |       .forEach(t => expect(html).toContain(`data-tab="${t}"`));
  89  |   });
  90  | 
  91  |   test('15 customer form fields present', () => {
  92  |     ['f-name','f-proprietor','f-display','f-gstin','f-pan','f-phone','f-email',
  93  |      'f-website','f-industry','f-group','f-territory','f-source','f-currency',
> 94  |      'f-terms','f-aliases','f-notes'].forEach(id => expect(html).toContain(`id="${id}"`));
      |                                                                  ^ Error: expect(received).toContain(expected) // indexOf
  95  |   });
  96  | 
  97  |   test('lookup datalists present', () => {
  98  |     expect(html).toContain('id="dl-industries"');
  99  |     expect(html).toContain('id="dl-groups"');
  100 |     expect(html).toContain('id="dl-territories"');
  101 |   });
  102 | 
  103 |   test('lead modal + opp modal present', () => {
  104 |     expect(html).toContain('id="modal-lead"');
  105 |     expect(html).toContain('id="modal-opp"');
  106 |     expect(html).toContain('id="modal-contact"');
  107 |     expect(html).toContain('id="modal-address"');
  108 |   });
  109 | });
  110 | 
  111 | // ──────────────────────────────────────────────────────────────
  112 | // SEED-DATA SANITY — verify test dataset is queryable
  113 | // ──────────────────────────────────────────────────────────────
  114 | test.describe(`${PWA_NAME} — Seed dataset`, () => {
  115 |   test('test customers exist in customers_v', async () => {
  116 |     const ctx = await request.newContext();
  117 |     const r = await ctx.get(`${DB}/customers_v?customer_id=like.cm-test-*&select=customer_id,name,customer_type,contact_count,address_count`, {
  118 |       headers: { 'Accept-Profile': 'sales' }
  119 |     });
  120 |     const rows = await r.json();
  121 |     expect(rows.length).toBeGreaterThanOrEqual(8);
  122 |     const park = rows.find(c => c.customer_id === 'cm-test-park-hospital');
  123 |     expect(park).toBeTruthy();
  124 |     expect(park.contact_count).toBeGreaterThanOrEqual(3);
  125 |     expect(park.address_count).toBeGreaterThanOrEqual(2);
  126 |     await ctx.dispose();
  127 |   });
  128 | 
  129 |   test('test leads cover all 4 active statuses', async () => {
  130 |     const ctx = await request.newContext();
  131 |     const r = await ctx.get(`${DB}/leads?lead_id=like.cm-test-*&select=status`, {
  132 |       headers: { 'Accept-Profile': 'sales' }
  133 |     });
  134 |     const rows = await r.json();
  135 |     const statuses = new Set(rows.map(r => r.status));
  136 |     expect(statuses.has('new')).toBeTruthy();
  137 |     expect(statuses.has('contacted')).toBeTruthy();
  138 |     expect(statuses.has('qualified')).toBeTruthy();
  139 |     expect(statuses.has('disqualified')).toBeTruthy();
  140 |     await ctx.dispose();
  141 |   });
  142 | 
  143 |   test('opportunities span all 5 stages', async () => {
  144 |     const ctx = await request.newContext();
  145 |     const r = await ctx.get(`${DB}/opportunities?opp_id=like.cm-test-*&select=stage,expected_value`, {
  146 |       headers: { 'Accept-Profile': 'sales' }
  147 |     });
  148 |     const rows = await r.json();
  149 |     const stages = new Set(rows.map(r => r.stage));
  150 |     expect(stages.has('qualification')).toBeTruthy();
  151 |     expect(stages.has('proposal')).toBeTruthy();
  152 |     expect(stages.has('negotiation')).toBeTruthy();
  153 |     expect(stages.has('won')).toBeTruthy();
  154 |     expect(stages.has('lost')).toBeTruthy();
  155 |     await ctx.dispose();
  156 |   });
  157 | 
  158 |   test('park-hospital 360 reflects offers + opps + comms', async () => {
  159 |     const ctx = await request.newContext();
  160 |     const r = await ctx.get(`${DB}/customer_360?customer_id=eq.cm-test-park-hospital`, {
  161 |       headers: { 'Accept-Profile': 'sales' }
  162 |     });
  163 |     const [s] = await r.json();
  164 |     expect(s).toBeTruthy();
  165 |     expect(Number(s.opp_count)).toBeGreaterThanOrEqual(2);  // opp-1 + opp-6
  166 |     expect(Number(s.opp_won)).toBeGreaterThanOrEqual(1);
  167 |     expect(Number(s.won_value)).toBeGreaterThanOrEqual(520000);
  168 |     expect(Number(s.comm_count)).toBeGreaterThanOrEqual(2);
  169 |     await ctx.dispose();
  170 |   });
  171 | 
  172 |   test('pipeline_by_owner has harish + pramod rows', async () => {
  173 |     const ctx = await request.newContext();
  174 |     const r = await ctx.get(`${DB}/pipeline_by_owner?owner_id=in.(harish,pramod)`, {
  175 |       headers: { 'Accept-Profile': 'sales' }
  176 |     });
  177 |     const rows = await r.json();
  178 |     expect(rows.length).toBe(2);
  179 |     rows.forEach(row => {
  180 |       expect(Number(row.open_count) + Number(row.won_count) + Number(row.lost_count)).toBeGreaterThan(0);
  181 |     });
  182 |     await ctx.dispose();
  183 |   });
  184 | });
  185 | 
  186 | // ──────────────────────────────────────────────────────────────
  187 | // HAPPY PATH — RENDERED UI
  188 | // ──────────────────────────────────────────────────────────────
  189 | test.describe(`${PWA_NAME} — Rendered UI (happy path)`, () => {
  190 |   test('list renders and search filters live', async ({ page }) => {
  191 |     await injectSession(page);
  192 |     await page.goto(BASE_URL);
  193 |     await expect(page.locator('#s-list')).toBeVisible();
  194 |     // tile shows >=8 customers
```