# 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: tour_pg_sharing.spec.js >> Tour Playground — Phase 4.17 org-wide sharing + Dexie search >> User A saves a SHARED tour via the real Save modal (unchecked = shared by default)
- Location: tests/tour_pg_sharing.spec.js:82:3

# Error details

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

Locator: locator('#toast')
Timeout: 5000ms
- Expected substring  - 1
+ Received string     + 2

- Tour saved
+ Save failed: /db/tours: 404 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+ <html><he

Call log:
  - Expect "toContainText" with timeout 5000ms
  - waiting for locator('#toast')
    10 × locator resolved to <div id="toast" class="show err">Save failed: /db/tours: 404 <!DOCTYPE HTML PUBLIC…</div>
       - unexpected value "Save failed: /db/tours: 404 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html><he"
    4 × locator resolved to <div class="" id="toast">Save failed: /db/tours: 404 <!DOCTYPE HTML PUBLIC…</div>
      - unexpected value "Save failed: /db/tours: 404 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html><he"

```

```yaml
- text: "Save failed: /db/tours: 404 <!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\"> <html><he"
```

# Test source

```ts
  1   | // tour_pg_sharing.spec.js — Phase 4.17 org-wide History sharing + Dexie search
  2   | // Drives the real UI (Save modal, History tabs, search field) as two different
  3   | // logged-in users to verify: shared-by-default visibility, "Created by X"
  4   | // ownership badge, owner-only delete, private opt-out, and search filtering.
  5   | const { test, expect } = require('@playwright/test');
  6   | 
  7   | const BASE_URL = 'https://dev.srv1111289.hstgr.cloud/tour-pg/';
  8   | const PROXY    = 'https://dev.srv1111289.hstgr.cloud/tour-pg-proxy';
  9   | 
  10  | function injectSession(page, id, name) {
  11  |   return page.addInitScript(({ id, name }) => {
  12  |     localStorage.setItem('lm360-session', JSON.stringify({
  13  |       empId: id, name, role: 'admin', loginAt: new Date().toISOString()
  14  |     }));
  15  |   }, { id, name });
  16  | }
  17  | 
  18  | function mintHubAuth(id, name) {
  19  |   const session = { empId: id, name, role: 'admin', loginAt: new Date().toISOString() };
  20  |   return 'Hub ' + Buffer.from(JSON.stringify(session)).toString('base64');
  21  | }
  22  | 
  23  | const USER_A = { id: 'pwtest-a', name: 'PW Test Alice' };
  24  | const USER_B = { id: 'pwtest-b', name: 'PW Test Bob' };
  25  | 
  26  | // Cleans up any leftover PW-TEST-* rows from a previous failed run, for both
  27  | // test users, across all 3 entity types, so the suite is idempotent.
  28  | async function cleanup(request) {
  29  |   const kinds = [
  30  |     { path: 'tours',    field: 'title',    owner: 'emp_id' },
  31  |     { path: 'uploads',  field: 'filename', owner: 'emp_id' },
  32  |     { path: 'batches',  field: 'name',     owner: 'created_by' },
  33  |   ];
  34  |   for (const u of [USER_A, USER_B]) {
  35  |     const auth = mintHubAuth(u.id, u.name);
  36  |     for (const k of kinds) {
  37  |       const r = await request.get(`${PROXY}/db/${k.path}`, { headers: { Authorization: auth } });
  38  |       if (!r.ok()) continue;
  39  |       const rows = await r.json();
  40  |       for (const row of rows) {
  41  |         if (row[k.field] && row[k.field].startsWith('PW-TEST-') && String(row[k.owner]) === u.id) {
  42  |           await request.delete(`${PROXY}/db/${k.path}/${row.id}`, { headers: { Authorization: auth } });
  43  |         }
  44  |       }
  45  |     }
  46  |   }
  47  | }
  48  | 
  49  | // Plants a 3-stop optimised state directly (skips file picker + real geocoding
  50  | // isn't needed since coords are hardcoded), matching the existing suite's
  51  | // "full state pipeline via page.evaluate" pattern in tour_pg.spec.js.
  52  | async function planTinyTour(page) {
  53  |   await page.goto(BASE_URL);
  54  |   await page.waitForTimeout(400);
  55  |   await page.evaluate(async () => {
  56  |     state.stops = [
  57  |       { id: 's1', name: 'Ambala', city: 'Ambala', address: 'Ambala', location: { lat: 30.3782, lng: 76.7767 }, included: true, provider: 'excel', raw: {} },
  58  |       { id: 's2', name: 'Sec 17', city: 'Chandigarh', address: 'Sec 17 Chd', location: { lat: 30.7333, lng: 76.7794 }, included: true, provider: 'excel', raw: {} },
  59  |     ];
  60  |     state.origin = { text: 'Mohali', location: { lat: 30.7046, lng: 76.7178 }, provider: 'excel' };
  61  |     state.roundTrip = true;
  62  |     state.currentTotal = { distance_m: 42000, duration_s: 3600 };
  63  |     state.baseTotal = { ...state.currentTotal };
  64  |     showScreen('s-editor');
  65  |     renderEditor();
  66  |   });
  67  |   await expect(page.locator('#s-editor')).toBeVisible();
  68  | }
  69  | 
  70  | test.describe('Tour Playground — Phase 4.17 org-wide sharing + Dexie search', () => {
  71  |   test.beforeAll(async ({ playwright }) => {
  72  |     const request = await playwright.request.newContext();
  73  |     await cleanup(request);
  74  |     await request.dispose();
  75  |   });
  76  |   test.afterAll(async ({ playwright }) => {
  77  |     const request = await playwright.request.newContext();
  78  |     await cleanup(request);
  79  |     await request.dispose();
  80  |   });
  81  | 
  82  |   test('User A saves a SHARED tour via the real Save modal (unchecked = shared by default)', async ({ page }) => {
  83  |     await injectSession(page, USER_A.id, USER_A.name);
  84  |     await planTinyTour(page);
  85  | 
  86  |     await page.click('button[title="Save tour to account"]');
  87  |     await expect(page.locator('#modal-save-tour')).toBeVisible();
  88  |     await expect(page.locator('#chk-save-tour-private')).not.toBeChecked(); // default unchecked = shared
  89  |     await page.fill('#inp-save-tour-title', 'PW-TEST-SHARED');
  90  |     await page.click('#modal-save-tour >> text=✓ Save tour');
> 91  |     await expect(page.locator('#toast')).toContainText('Tour saved');
      |                                          ^ Error: expect(locator).toContainText(expected) failed
  92  |   });
  93  | 
  94  |   test('User A saves a PRIVATE tour via the modal (checkbox checked)', async ({ page }) => {
  95  |     await injectSession(page, USER_A.id, USER_A.name);
  96  |     await planTinyTour(page);
  97  | 
  98  |     await page.click('button[title="Save tour to account"]');
  99  |     await page.fill('#inp-save-tour-title', 'PW-TEST-PRIVATE');
  100 |     await page.check('#chk-save-tour-private');
  101 |     await page.click('#modal-save-tour >> text=✓ Save tour');
  102 |     await expect(page.locator('#toast')).toContainText('Tour saved');
  103 |   });
  104 | 
  105 |   test('User B sees the SHARED tour with "Created by" badge, no delete button, but NOT the private one', async ({ page }) => {
  106 |     await injectSession(page, USER_B.id, USER_B.name);
  107 |     await page.goto(BASE_URL);
  108 |     await page.waitForTimeout(300);
  109 |     await page.evaluate(() => openHistory());
  110 |     await page.click('#tab-tours');
  111 |     await page.waitForTimeout(600);
  112 | 
  113 |     const sharedItem = page.locator('#hist-tours-list .hist-item', { hasText: 'PW-TEST-SHARED' });
  114 |     await expect(sharedItem).toBeVisible();
  115 |     await expect(sharedItem).toContainText('Created by PW Test Alice');
  116 |     await expect(sharedItem.locator('.hist-btn.danger')).toHaveCount(0); // no delete for non-owner
  117 | 
  118 |     await expect(page.locator('#hist-tours-list .hist-item', { hasText: 'PW-TEST-PRIVATE' })).toHaveCount(0);
  119 |   });
  120 | 
  121 |   test('User B\'s Dexie search filters to the shared tour, including by creator name', async ({ page }) => {
  122 |     await injectSession(page, USER_B.id, USER_B.name);
  123 |     await page.goto(BASE_URL);
  124 |     await page.waitForTimeout(300);
  125 |     await page.evaluate(() => openHistory());
  126 |     await page.click('#tab-tours');
  127 |     await page.waitForTimeout(600);
  128 | 
  129 |     await page.fill('#search-tours', 'PW-TEST-SHARED');
  130 |     await page.waitForTimeout(200);
  131 |     await expect(page.locator('#hist-tours-list .hist-item')).toHaveCount(1);
  132 |     await expect(page.locator('#hist-tours-list .hist-item')).toContainText('PW-TEST-SHARED');
  133 | 
  134 |     // Search by the creator's name instead of the title
  135 |     await page.fill('#search-tours', 'Alice');
  136 |     await page.waitForTimeout(200);
  137 |     await expect(page.locator('#hist-tours-list .hist-item', { hasText: 'PW-TEST-SHARED' })).toBeVisible();
  138 | 
  139 |     // A query matching nothing shows the no-match empty state, not stale rows
  140 |     await page.fill('#search-tours', 'zzz-no-such-tour-zzz');
  141 |     await page.waitForTimeout(200);
  142 |     await expect(page.locator('#hist-tours-list .empty')).toContainText('No tours match your search');
  143 |   });
  144 | 
  145 |   test('multi-token search: "BTC himach" matches a title with both words, not adjacent', async ({ page }) => {
  146 |     await injectSession(page, USER_A.id, USER_A.name);
  147 |     await planTinyTour(page);
  148 |     await page.click('button[title="Save tour to account"]');
  149 |     await page.fill('#inp-save-tour-title', 'PW-TEST-Verve-BTC-Batch-Himachal');
  150 |     await page.click('#modal-save-tour >> text=✓ Save tour');
  151 |     await expect(page.locator('#toast')).toContainText('Tour saved');
  152 | 
  153 |     await page.goto(BASE_URL);
  154 |     await page.waitForTimeout(300);
  155 |     await page.evaluate(() => openHistory());
  156 |     await page.click('#tab-tours');
  157 |     await page.waitForTimeout(600);
  158 | 
  159 |     // Neither word appears adjacent to the other in the title, and the query
  160 |     // has different capitalization/word order than the stored text.
  161 |     await page.fill('#search-tours', 'BTC himach');
  162 |     await page.waitForTimeout(200);
  163 |     await expect(page.locator('#hist-tours-list .hist-item', { hasText: 'PW-TEST-Verve-BTC-Batch-Himachal' })).toBeVisible();
  164 | 
  165 |     // A query where only ONE of the two tokens matches must NOT match (AND, not OR).
  166 |     await page.fill('#search-tours', 'BTC nonexistentword');
  167 |     await page.waitForTimeout(200);
  168 |     await expect(page.locator('#hist-tours-list .hist-item', { hasText: 'PW-TEST-Verve-BTC-Batch-Himachal' })).toHaveCount(0);
  169 |   });
  170 | 
  171 |   test('multi-token search on Uploads tab: "BTC himach" matches a filename with both words', async ({ page }) => {
  172 |     await injectSession(page, USER_A.id, USER_A.name);
  173 |     await planTinyTour(page);
  174 |     await page.evaluate(async () => {
  175 |       _currentFilename = 'PW-TEST-Verve-BTC-Batch-Himachal.xlsx';
  176 |       await autoSaveUpload();
  177 |     });
  178 | 
  179 |     await page.goto(BASE_URL);
  180 |     await page.waitForTimeout(300);
  181 |     await page.evaluate(() => openHistory()); // defaults to the Uploads tab
  182 |     await page.waitForTimeout(600);
  183 | 
  184 |     await page.fill('#search-uploads', 'BTC himach');
  185 |     await page.waitForTimeout(200);
  186 |     await expect(page.locator('#hist-uploads-list .hist-item', { hasText: 'PW-TEST-Verve-BTC-Batch-Himachal' })).toBeVisible();
  187 | 
  188 |     await page.fill('#search-uploads', 'BTC nonexistentword');
  189 |     await page.waitForTimeout(200);
  190 |     await expect(page.locator('#hist-uploads-list .hist-item', { hasText: 'PW-TEST-Verve-BTC-Batch-Himachal' })).toHaveCount(0);
  191 |   });
```