# 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: tests/ui/entities.spec.ts >> us.elect.info entity sections >> states section - index and random pages
- Location: tests/ui/entities.spec.ts:154:7

# Error details

```
Error: expect(locator).toBeVisible() failed

Locator: locator('#map .leaflet-interactive').first()
Expected: visible
Timeout: 15000ms
Error: element(s) not found

Call log:
  - Expect "toBeVisible" locator('#map .leaflet-interactive').first() with timeout 15000ms
  - waiting for locator('#map .leaflet-interactive').first()

```

```yaml
- link "← Back to Home":
  - /url: /
- banner:
  - heading "States & Territories" [level=1]
  - paragraph: US states in federal elections
- button "Map"
- button "Grid"
- main:
  - button "Zoom in"
  - button "Zoom out"
  - link "Leaflet":
    - /url: https://leafletjs.com
  - text: ©
  - link "OpenStreetMap":
    - /url: https://www.openstreetmap.org/copyright
```

# Test source

```ts
  61  |     const randomLinks = getRandomItems(hrefs.filter(Boolean) as string[], 3);
  62  | 
  63  |     for (const href of randomLinks) {
  64  |       const entityResponse = await page.goto(toRelativePath(href));
  65  |       expect(entityResponse?.status()).toBe(200);
  66  |       await expect(page.locator('h1')).toBeVisible();
  67  |     }
  68  |   });
  69  | 
  70  |   test('parties section - index and random pages', async ({ page }) => {
  71  |     const indexResponse = await page.goto('/parties/');
  72  |     expect(indexResponse?.status()).toBe(200);
  73  |     await expect(page.locator('h1')).toContainText(/Parties/i);
  74  | 
  75  |     // Wait for GraphQL browseEntities to populate results
  76  |     await expect(page.locator('.entity-search-result').first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  77  | 
  78  |     const links = await page.locator('.entity-search-result').all();
  79  |     expect(links.length).toBeGreaterThan(0);
  80  | 
  81  |     const hrefs = await Promise.all(links.slice(0, 20).map(l => l.getAttribute('href')));
  82  |     const randomLinks = getRandomItems(hrefs.filter(Boolean) as string[], 3);
  83  | 
  84  |     for (const href of randomLinks) {
  85  |       const entityResponse = await page.goto(toRelativePath(href));
  86  |       expect(entityResponse?.status()).toBe(200);
  87  |       await expect(page.locator('h1')).toBeVisible();
  88  |     }
  89  |   });
  90  | 
  91  |   test('offices section - index and random pages', async ({ page }) => {
  92  |     const indexResponse = await page.goto('/offices/');
  93  |     expect(indexResponse?.status()).toBe(200);
  94  |     await expect(page.locator('h1')).toContainText(/Offices/i);
  95  | 
  96  |     // Wait for GraphQL browseEntities to populate results
  97  |     await expect(page.locator('.entity-search-result').first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  98  | 
  99  |     const links = await page.locator('.entity-search-result').all();
  100 |     expect(links.length).toBeGreaterThan(0);
  101 | 
  102 |     const hrefs = await Promise.all(links.slice(0, 20).map(l => l.getAttribute('href')));
  103 |     const randomLinks = getRandomItems(hrefs.filter(Boolean) as string[], 3);
  104 | 
  105 |     for (const href of randomLinks) {
  106 |       const entityResponse = await page.goto(toRelativePath(href));
  107 |       expect(entityResponse?.status()).toBe(200);
  108 |       await expect(page.locator('h1')).toBeVisible();
  109 |     }
  110 |   });
  111 | 
  112 |   test('districts section - index and random pages', async ({ page }) => {
  113 |     const indexResponse = await page.goto('/districts/');
  114 |     expect(indexResponse?.status()).toBe(200);
  115 |     await expect(page.locator('h1')).toContainText(/Districts/i);
  116 | 
  117 |     // Map view is default — verify Leaflet map renders with GeoJSON state shapes
  118 |     await expect(page.locator('#map.leaflet-container')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  119 |     await expect(page.locator('#map .leaflet-interactive').first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  120 |     const shapes = await page.locator('#map .leaflet-interactive').all();
  121 |     expect(shapes.length).toBeGreaterThan(0);
  122 | 
  123 |     // Switch to List view to test state selector and district results
  124 |     await page.locator('.toggle-btn[data-view="list"]').click();
  125 | 
  126 |     // Districts list view loads state options from GraphQL, then requires selection
  127 |     const stateSelect = page.locator('#district-state-select');
  128 |     await expect(stateSelect).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  129 | 
  130 |     // Wait for state options to populate from GraphQL (more than just the placeholder)
  131 |     await page.waitForFunction(() => {
  132 |       const select = document.querySelector('#district-state-select') as HTMLSelectElement;
  133 |       return select && select.options.length > 1;
  134 |     }, { timeout: DATA_LOAD_TIMEOUT });
  135 | 
  136 |     await stateSelect.selectOption('CA');
  137 | 
  138 |     // Wait for district results to load
  139 |     await expect(page.locator('.district-result').first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  140 | 
  141 |     const links = await page.locator('.district-result').all();
  142 |     expect(links.length).toBeGreaterThan(0);
  143 | 
  144 |     const hrefs = await Promise.all(links.slice(0, 20).map(l => l.getAttribute('href')));
  145 |     const randomLinks = getRandomItems(hrefs.filter(Boolean) as string[], 3);
  146 | 
  147 |     for (const href of randomLinks) {
  148 |       const entityResponse = await page.goto(toRelativePath(href));
  149 |       expect(entityResponse?.status()).toBe(200);
  150 |       await expect(page.locator('h1')).toBeVisible();
  151 |     }
  152 |   });
  153 | 
  154 |   test('states section - index and random pages', async ({ page }) => {
  155 |     const indexResponse = await page.goto('/states/');
  156 |     expect(indexResponse?.status()).toBe(200);
  157 |     await expect(page.locator('h1')).toContainText(/States/i);
  158 | 
  159 |     // Map view is default — verify Leaflet map renders with GeoJSON state shapes
  160 |     await expect(page.locator('#map.leaflet-container')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
> 161 |     await expect(page.locator('#map .leaflet-interactive').first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
      |                                                                     ^ Error: expect(locator).toBeVisible() failed
  162 |     const shapes = await page.locator('#map .leaflet-interactive').all();
  163 |     expect(shapes.length).toBeGreaterThan(0);
  164 | 
  165 |     // Switch to Grid view to test state chips and detail navigation
  166 |     await page.locator('.toggle-btn[data-view="grid"]').click();
  167 | 
  168 |     await expect(page.locator('.state-chip').first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  169 | 
  170 |     const links = await page.locator('.state-chip').all();
  171 |     expect(links.length).toBeGreaterThan(0);
  172 | 
  173 |     const hrefs = await Promise.all(links.slice(0, 20).map(l => l.getAttribute('href')));
  174 |     const randomLinks = getRandomItems(hrefs.filter(Boolean) as string[], 3);
  175 | 
  176 |     for (const href of randomLinks) {
  177 |       const entityResponse = await page.goto(toRelativePath(href));
  178 |       expect(entityResponse?.status()).toBe(200);
  179 |       await expect(page.locator('h1')).toBeVisible();
  180 |     }
  181 |   });
  182 | 
  183 |   test('elections section - index and random pages', async ({ page }) => {
  184 |     const indexResponse = await page.goto('/elections/');
  185 |     expect(indexResponse?.status()).toBe(200);
  186 |     await expect(page.locator('h1')).toContainText(/Election/i);
  187 | 
  188 |     // Wait for GraphQL browseEntities to populate results
  189 |     await expect(page.locator('.entity-search-result').first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  190 | 
  191 |     const links = await page.locator('.entity-search-result').all();
  192 |     expect(links.length).toBeGreaterThan(0);
  193 | 
  194 |     const hrefs = await Promise.all(links.slice(0, 20).map(l => l.getAttribute('href')));
  195 |     const randomLinks = getRandomItems(hrefs.filter(Boolean) as string[], 3);
  196 | 
  197 |     for (const href of randomLinks) {
  198 |       const entityResponse = await page.goto(toRelativePath(href));
  199 |       expect(entityResponse?.status()).toBe(200);
  200 |       await expect(page.locator('h1')).toBeVisible();
  201 |     }
  202 |   });
  203 | });
  204 | 
  205 | test.describe('office profile - dynamic candidates', () => {
  206 |   // President of the United States office page
  207 |   const PRESIDENT_PATH = '/offices/684f6e2d-7105-932b-8b7f-94016580f727/';
  208 | 
  209 |   test('office profile loads candidates filtered to office', async ({ page }) => {
  210 |     // Intercept the browseEntities GraphQL request to verify filter is applied
  211 |     const responsePromise = page.waitForResponse(resp => {
  212 |       if (!resp.url().includes('/api/graphql')) return false;
  213 |       if (resp.request().method() !== 'POST') return false;
  214 |       const body = resp.request().postData() || '';
  215 |       return body.includes('browseEntities');
  216 |     }, { timeout: DATA_LOAD_TIMEOUT });
  217 | 
  218 |     await page.goto(PRESIDENT_PATH);
  219 |     const response = await responsePromise;
  220 | 
  221 |     // Verify the request includes the office filter
  222 |     const requestBody = JSON.parse(response.request().postData() || '{}');
  223 |     expect(requestBody.variables?.filter).toContain('office:P');
  224 | 
  225 |     // Verify the response has results
  226 |     const json = await response.json();
  227 |     const results = json.data?.browseEntities?.results || [];
  228 |     expect(results.length).toBeGreaterThan(0);
  229 |   });
  230 | 
  231 |   test('office profile candidates sorted by total contributions descending', async ({ page }) => {
  232 |     const responsePromise = page.waitForResponse(resp => {
  233 |       if (!resp.url().includes('/api/graphql')) return false;
  234 |       if (resp.request().method() !== 'POST') return false;
  235 |       const body = resp.request().postData() || '';
  236 |       return body.includes('browseEntities');
  237 |     }, { timeout: DATA_LOAD_TIMEOUT });
  238 | 
  239 |     await page.goto(PRESIDENT_PATH);
  240 |     const response = await responsePromise;
  241 | 
  242 |     const json = await response.json();
  243 |     const results = json.data?.browseEntities?.results || [];
  244 |     expect(results.length).toBeGreaterThan(1);
  245 | 
  246 |     // Extract total_contributions from metadata to verify sort order
  247 |     const contributions = results.map((r: { metadata?: string }) => {
  248 |       try {
  249 |         const meta = JSON.parse(r.metadata || '{}');
  250 |         return meta.total_contributions || 0;
  251 |       } catch { return 0; }
  252 |     });
  253 | 
  254 |     for (let i = 1; i < contributions.length; i++) {
  255 |       expect(contributions[i - 1]).toBeGreaterThanOrEqual(contributions[i]);
  256 |     }
  257 |   });
  258 | 
  259 |   test('office profile renders candidate results in UI', async ({ page }) => {
  260 |     await page.goto(PRESIDENT_PATH);
  261 | 
```