# 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 — Rendered UI (happy path) >> Add-customer form has Save button and chip toggle
- Location: tests/customers.spec.js:277:3

# Error details

```
Test timeout of 30000ms exceeded.
```

```
Error: page.click: Test timeout of 30000ms exceeded.
Call log:
  - waiting for locator('#nav-add')

```

# Page snapshot

```yaml
- generic [ref=e2]: 404 page not found
```

# Test source

```ts
  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
  195 |     await expect(page.locator('#t-total')).not.toHaveText('—');
  196 |     await expect.poll(async () => Number(await page.locator('#t-total').textContent())).toBeGreaterThanOrEqual(8);
  197 |     // search for "park"
  198 |     await page.fill('#search-q', 'park');
  199 |     await page.waitForTimeout(300);
  200 |     const rows = await page.locator('.c-row').count();
  201 |     expect(rows).toBeGreaterThanOrEqual(1);
  202 |     expect(rows).toBeLessThan(8);
  203 |     await expect(page.locator('.c-row-name').first()).toContainText(/Park/i);
  204 |   });
  205 | 
  206 |   test('type chip filters to Individual only', async ({ page }) => {
  207 |     await injectSession(page);
  208 |     await page.goto(BASE_URL);
  209 |     await page.waitForSelector('.c-row');
  210 |     await page.click('.chip[data-type="individual"]');
  211 |     await page.waitForTimeout(150);
  212 |     const badges = await page.locator('.c-row-badge').allTextContents();
  213 |     expect(badges.length).toBeGreaterThan(0);
  214 |     badges.forEach(b => expect(b).toMatch(/Individual/i));
  215 |   });
  216 | 
  217 |   test('detail screen opens with all 7 tabs', async ({ page }) => {
  218 |     await injectSession(page);
  219 |     await page.goto(BASE_URL);
  220 |     await page.waitForSelector('.c-row');
  221 |     await page.fill('#search-q', 'park');
  222 |     await page.waitForTimeout(300);
  223 |     await page.locator('.c-row').first().click();
  224 |     await expect(page.locator('#s-detail')).toBeVisible();
  225 |     for (const t of ['info','contacts','addresses','jobs','offers','deals','comms']) {
  226 |       await expect(page.locator(`#s-detail .chip[data-tab="${t}"]`)).toBeVisible();
  227 |     }
  228 |   });
  229 | 
  230 |   test('360 card renders on Info tab', async ({ page }) => {
  231 |     await injectSession(page);
  232 |     await page.goto(BASE_URL);
  233 |     await page.waitForSelector('.c-row');
  234 |     await page.fill('#search-q', 'park');
  235 |     await page.waitForTimeout(300);
  236 |     await page.locator('.c-row').first().click();
  237 |     await page.waitForTimeout(400);
  238 |     await expect(page.locator('.c360-card')).toBeVisible();
  239 |     // 6 number cells + 2 money cells
  240 |     await expect(page.locator('.c360-cell')).toHaveCount(8);
  241 |   });
  242 | 
  243 |   test('Pipeline screen opens with leads kanban + summary tiles', async ({ page }) => {
  244 |     await injectSession(page);
  245 |     await page.goto(BASE_URL);
  246 |     await page.waitForSelector('#nav-pipeline');
  247 |     await page.click('#nav-pipeline');
  248 |     await expect(page.locator('#s-pipeline')).toBeVisible();
  249 |     await expect(page.locator('#kanban-leads')).toBeVisible();
  250 |     await expect(page.locator('#kanban-leads .kcol')).toHaveCount(5);
  251 |     // Switch to Opps
  252 |     await page.click('.pipe-tab[data-pt="opps"]');
  253 |     await expect(page.locator('#kanban-opps')).toBeVisible();
  254 |     await expect(page.locator('#kanban-opps .kcol')).toHaveCount(5);
  255 |     // By-Owner table appears on Opps tab
  256 |     await expect(page.locator('.bo-table')).toBeVisible();
  257 |   });
  258 | 
  259 |   test('owner filter chips render and filter Leads kanban', async ({ page }) => {
  260 |     await injectSession(page);
  261 |     await page.goto(BASE_URL);
  262 |     await page.waitForSelector('#nav-pipeline');
  263 |     await page.click('#nav-pipeline');
  264 |     await page.waitForTimeout(500);
  265 |     await expect(page.locator('#owner-chips')).toBeVisible();
  266 |     // total cards before filter
  267 |     const before = await page.locator('#kanban-leads .kcard').count();
  268 |     expect(before).toBeGreaterThan(0);
  269 |     // click harish chip
  270 |     await page.locator('#owner-chips .chip', { hasText: /Harish/i }).first().click();
  271 |     await page.waitForTimeout(150);
  272 |     const after = await page.locator('#kanban-leads .kcard').count();
  273 |     expect(after).toBeLessThanOrEqual(before);
  274 |     expect(after).toBeGreaterThan(0);
  275 |   });
  276 | 
  277 |   test('Add-customer form has Save button and chip toggle', async ({ page }) => {
  278 |     await injectSession(page);
  279 |     await page.goto(BASE_URL);
> 280 |     await page.click('#nav-add');
      |                ^ Error: page.click: Test timeout of 30000ms exceeded.
  281 |     await expect(page.locator('#s-form')).toBeVisible();
  282 |     await expect(page.locator('#f-save-btn')).toBeVisible();
  283 |     await expect(page.locator('#s-form .chip[data-ftype="business"]')).toBeVisible();
  284 |     await expect(page.locator('#s-form .chip[data-ftype="individual"]')).toBeVisible();
  285 |   });
  286 | });
  287 | 
  288 | // ──────────────────────────────────────────────────────────────
  289 | // CRUD — UI driven create + verify + cleanup
  290 | // ──────────────────────────────────────────────────────────────
  291 | test.describe(`${PWA_NAME} — UI CRUD`, () => {
  292 |   const testName = `Playwright Probe ${RUN_ID}`;
  293 | 
  294 |   test('create customer via form, appears in list, delete via DB', async ({ page }) => {
  295 |     await injectSession(page);
  296 |     await page.goto(BASE_URL);
  297 |     await page.click('#nav-add');
  298 |     await page.fill('#f-name', testName);
  299 |     await page.fill('#f-phone', '9999000111');
  300 |     await page.fill('#f-industry', 'Hospitality');
  301 |     await page.evaluate(() => saveForm());
  302 |     await page.waitForTimeout(1200);
  303 |     // Should land back on list with toast then refreshed list
  304 |     await page.fill('#search-q', RUN_ID);
  305 |     await page.waitForTimeout(300);
  306 |     await expect(page.locator('.c-row-name').first()).toContainText(testName);
  307 | 
  308 |     // Cleanup via DB
  309 |     await dbDelete('customers', `name=eq.${encodeURIComponent(testName)}`);
  310 |   });
  311 | 
  312 |   test('empty form save shows validation toast', async ({ page }) => {
  313 |     await injectSession(page);
  314 |     await page.goto(BASE_URL);
  315 |     await page.click('#nav-add');
  316 |     await page.evaluate(() => saveForm());
  317 |     // Form should still be visible — validation prevented save
  318 |     await page.waitForTimeout(400);
  319 |     await expect(page.locator('#s-form')).toBeVisible();
  320 |     // Toast should have triggered err class within the visible window
  321 |     const sawErr = await page.evaluate(() => document.getElementById('toast').className.includes('err'));
  322 |     expect(sawErr).toBeTruthy();
  323 |   });
  324 | });
  325 | 
  326 | // ──────────────────────────────────────────────────────────────
  327 | // ACCESS GATE
  328 | // ──────────────────────────────────────────────────────────────
  329 | test.describe(`${PWA_NAME} — Access`, () => {
  330 |   test('user without grant sees restricted screen', async ({ page }) => {
  331 |     // Use an empId that has no customers access grant
  332 |     await injectSession(page, 'amit', 'Amit');
  333 |     await page.goto(BASE_URL);
  334 |     await page.waitForTimeout(800);
  335 |     await expect(page.locator('text=Access Restricted')).toBeVisible();
  336 |   });
  337 | });
  338 | 
```