# 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: vendors.spec.js >> Vendor Management — Catalog tab (Phase 3a) >> HTML contains catalog tab + link button + modals
- Location: tests/vendors.spec.js:366:3

# Error details

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

Expected substring: "data-tab=\"catalog\""
Received string:    "404 page not found
"
```

# Test source

```ts
  270 | 
  271 |   test('Add tab opens empty form', async ({ page }) => {
  272 |     await page.locator('#nav-add').click();
  273 |     await expect(page.locator('#s-form.active')).toBeVisible();
  274 |     await expect(page.locator('#f-name')).toHaveValue('');
  275 |     await expect(page.locator('#f-title')).toHaveText('Add Vendor');
  276 |   });
  277 | 
  278 |   test('saving with empty name shows error toast', async ({ page }) => {
  279 |     await page.locator('#nav-add').click();
  280 |     await page.locator('#f-save-btn').click();
  281 |     await expect(page.locator('#toast.show')).toContainText('required', { timeout: 4000 });
  282 |   });
  283 | 
  284 |   test('full add → detail → edit → soft-delete flow', async ({ page }) => {
  285 |     // Add
  286 |     await page.locator('#nav-add').click();
  287 |     await page.locator('#f-name').fill(TEST_NAME);
  288 |     await page.locator('#f-type').selectOption('vendor');
  289 |     await page.locator('#f-proprietor').fill('PW Proprietor');
  290 |     await page.locator('#f-upi-receipt').fill('PW UPI');
  291 |     await page.locator('#f-phone').fill('9999000011');
  292 |     await page.locator('#f-email').fill('pw@test.in');
  293 |     await page.locator('#f-gst').fill('29ABCDE1234F1Z5');  // 15 chars (valid GSTIN length)
  294 |     await page.locator('#f-pan').fill('ABCDE1234F');         // 10 chars (valid PAN length)
  295 |     await page.locator('#f-contact').fill('PW Contact');
  296 |     await page.locator('#f-address').fill('PW Address');
  297 |     await page.locator('#f-aliases').fill('PW Alias 1, PW Alias 2');
  298 |     await page.locator('#f-save-btn').click();
  299 |     await expect(page.locator('#toast.ok')).toContainText('added', { timeout: 8000 });
  300 |     // Returns to list
  301 |     await expect(page.locator('#s-list.active')).toBeVisible();
  302 | 
  303 |     // Verify in list via search
  304 |     await page.locator('#search-q').fill(TEST_NAME);
  305 |     await page.waitForTimeout(300);
  306 |     await expect(page.locator('.v-row', { hasText: TEST_NAME })).toBeVisible({ timeout: 4000 });
  307 | 
  308 |     // Open detail
  309 |     await page.locator('.v-row', { hasText: TEST_NAME }).click();
  310 |     await expect(page.locator('#d-title')).toHaveText(TEST_NAME);
  311 |     // Verify proprietor + GST visible in info
  312 |     await expect(page.locator('#d-info-card')).toContainText('PW Proprietor');
  313 |     await expect(page.locator('#d-info-card')).toContainText('29ABCDE1234F1Z5');
  314 | 
  315 |     // Edit
  316 |     await page.locator('#s-detail .hdr-btn:has-text("Edit")').click();
  317 |     await expect(page.locator('#f-title')).toHaveText('Edit Vendor');
  318 |     await page.locator('#f-address').fill('PW Address — UPDATED');
  319 |     await page.locator('#f-save-btn').click();
  320 |     await expect(page.locator('#toast.ok')).toContainText('Saved', { timeout: 8000 });
  321 |     // Returns to detail; verify update
  322 |     await expect(page.locator('#d-info-card')).toContainText('UPDATED', { timeout: 4000 });
  323 | 
  324 |     // Soft-delete from edit form
  325 |     await page.locator('#s-detail .hdr-btn:has-text("Edit")').click();
  326 |     await page.locator('#f-delete-row button').click();
  327 |     // Accept the confirm dialog
  328 |     page.on('dialog', d => d.accept());
  329 |     await page.locator('#f-delete-row button').click();
  330 |     await expect(page.locator('#toast.ok')).toContainText('deactivated', { timeout: 8000 });
  331 |     // Should be back on list
  332 |     await expect(page.locator('#s-list.active')).toBeVisible();
  333 |     // Vendor should no longer appear in active list
  334 |     await page.locator('#search-q').fill(TEST_NAME);
  335 |     await page.waitForTimeout(300);
  336 |     await expect(page.locator('.v-row', { hasText: TEST_NAME })).toHaveCount(0);
  337 |   });
  338 | });
  339 | 
  340 | // ─── SEARCH ACROSS FIELDS ───────────────────────────────────────────
  341 | test.describe(`${PWA_NAME} — Search across all fields (client-side)`, () => {
  342 |   test.beforeEach(async ({ page }) => {
  343 |     await injectSession(page, 'harish', 'Harish Kumar Lal');
  344 |     await page.goto(BASE_URL);
  345 |     await expect(page.locator('.v-row').first()).toBeVisible({ timeout: 8000 });
  346 |   });
  347 | 
  348 |   test('search by partial phone matches', async ({ page }) => {
  349 |     // Use an existing payee's phone if available; just confirm the input drives filter logic
  350 |     await page.locator('#search-q').fill('98');
  351 |     await page.waitForTimeout(300);
  352 |     const subText = await page.locator('#list-sub').textContent();
  353 |     expect(subText).toMatch(/of \d+/);
  354 |   });
  355 | 
  356 |   test('search by alias (existing payee with aliases)', async ({ page }) => {
  357 |     // Most payees may have no aliases; just confirm the input accepts and updates count
  358 |     await page.locator('#search-q').fill('xyz_nope_alias_q');
  359 |     await page.waitForTimeout(300);
  360 |     await expect(page.locator('#v-list .empty')).toBeVisible();
  361 |   });
  362 | });
  363 | 
  364 | // ─── PHASE 3a: CATALOG TAB ──────────────────────────────────────────
  365 | test.describe(`${PWA_NAME} — Catalog tab (Phase 3a)`, () => {
  366 |   test('HTML contains catalog tab + link button + modals', async () => {
  367 |     const ctx = await request.newContext();
  368 |     const resp = await ctx.get(BASE_URL);
  369 |     const html = await resp.text();
> 370 |     expect(html).toContain('data-tab="catalog"');
      |                  ^ Error: expect(received).toContain(expected) // indexOf
  371 |     expect(html).toContain('id="d-panel-catalog"');
  372 |     expect(html).toContain('id="cat-list"');
  373 |     expect(html).toContain('id="modal-link-product"');
  374 |     expect(html).toContain('id="modal-vc-edit"');
  375 |     expect(html).toContain('id="lp-search"');
  376 |     expect(html).toContain('id="lp-add-name"');
  377 |     expect(html).toContain('id="vce-price"');
  378 |     await ctx.dispose();
  379 |   });
  380 | 
  381 |   test('Catalog tab loads on detail screen', async ({ page }) => {
  382 |     await injectSession(page, 'harish', 'Harish Kumar Lal');
  383 |     await page.goto(BASE_URL);
  384 |     await expect(page.locator('.v-row').first()).toBeVisible({ timeout: 8000 });
  385 |     await page.locator('.v-row').first().click();
  386 |     await page.locator('#s-detail .chip[data-tab="catalog"]').click();
  387 |     await expect(page.locator('#d-panel-catalog')).toBeVisible();
  388 |     // Either shows entries or empty state — both pass
  389 |     await expect(page.locator('#cat-list')).toBeVisible({ timeout: 4000 });
  390 |   });
  391 | 
  392 |   test('Link Product modal opens + shows results', async ({ page }) => {
  393 |     await injectSession(page, 'harish', 'Harish Kumar Lal');
  394 |     await page.goto(BASE_URL);
  395 |     await expect(page.locator('.v-row').first()).toBeVisible({ timeout: 8000 });
  396 |     await page.locator('.v-row').first().click();
  397 |     await page.locator('#s-detail .chip[data-tab="catalog"]').click();
  398 |     await page.locator('button:has-text("Link a product")').click();
  399 |     await expect(page.locator('#modal-link-product.open')).toBeVisible({ timeout: 3000 });
  400 |     // Wait for catalog results to populate
  401 |     await expect(page.locator('.lp-result').first()).toBeVisible({ timeout: 4000 });
  402 |   });
  403 | });
  404 | 
  405 | // ─── PHASE 3c: SUPPLIES + RATE CARDS + PRICES ───────────────────────
  406 | test.describe(`${PWA_NAME} — Phase 3c tabs`, () => {
  407 |   test('HTML contains all 3 new tabs + panels + modal', async () => {
  408 |     const ctx = await request.newContext();
  409 |     const resp = await ctx.get(BASE_URL);
  410 |     const html = await resp.text();
  411 |     expect(html).toContain('data-tab="supplies"');
  412 |     expect(html).toContain('data-tab="rates"');
  413 |     expect(html).toContain('data-tab="prices"');
  414 |     expect(html).toContain('id="d-panel-supplies"');
  415 |     expect(html).toContain('id="d-panel-rates"');
  416 |     expect(html).toContain('id="d-panel-prices"');
  417 |     expect(html).toContain('id="modal-rate-card"');
  418 |     expect(html).toContain('id="rcm-search"');
  419 |     expect(html).toContain('id="rcm-price"');
  420 |     expect(html).toContain('function loadVendorSupplies');
  421 |     expect(html).toContain('function loadRateCards');
  422 |     expect(html).toContain('function loadPriceCompare');
  423 |     expect(html).toContain('vendor_supplies_v');
  424 |     expect(html).toContain('vendor_price_compare_v');
  425 |     expect(html).toContain('rate_cards_v');
  426 |     await ctx.dispose();
  427 |   });
  428 | 
  429 |   test('Supplies tab opens (loads even with no data)', async ({ page }) => {
  430 |     await injectSession(page, 'harish', 'Harish Kumar Lal');
  431 |     await page.goto(BASE_URL);
  432 |     await expect(page.locator('.v-row').first()).toBeVisible({ timeout: 8000 });
  433 |     await page.locator('.v-row').first().click();
  434 |     await page.locator('#s-detail .chip[data-tab="supplies"]').click();
  435 |     await expect(page.locator('#d-panel-supplies')).toBeVisible();
  436 |     // Tiles populate (or "—" if no data)
  437 |     await expect(page.locator('#sp-count')).toBeVisible();
  438 |   });
  439 | 
  440 |   test('Rate Card tab opens + Add modal works', async ({ page }) => {
  441 |     await injectSession(page, 'harish', 'Harish Kumar Lal');
  442 |     await page.goto(BASE_URL);
  443 |     await expect(page.locator('.v-row').first()).toBeVisible({ timeout: 8000 });
  444 |     await page.locator('.v-row').first().click();
  445 |     await page.locator('#s-detail .chip[data-tab="rates"]').click();
  446 |     await expect(page.locator('#d-panel-rates')).toBeVisible();
  447 |     await page.locator('button:has-text("Add rate card")').click();
  448 |     await expect(page.locator('#modal-rate-card.open')).toBeVisible({ timeout: 3000 });
  449 |     await expect(page.locator('#rcm-search')).toBeVisible();
  450 |   });
  451 | 
  452 |   test('Prices tab opens', async ({ page }) => {
  453 |     await injectSession(page, 'harish', 'Harish Kumar Lal');
  454 |     await page.goto(BASE_URL);
  455 |     await expect(page.locator('.v-row').first()).toBeVisible({ timeout: 8000 });
  456 |     await page.locator('.v-row').first().click();
  457 |     await page.locator('#s-detail .chip[data-tab="prices"]').click();
  458 |     await expect(page.locator('#d-panel-prices')).toBeVisible();
  459 |   });
  460 | });
  461 | 
```