# 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 - additional index + detail >> precincts section - map view and state filter
- Location: tests/ui/entities.spec.ts:546:7

# Error details

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

```
Error: page.goto: net::ERR_ABORTED; maybe frame was detached?
Call log:
  - navigating to "http://10.1.105.89/precincts/", waiting until "load"

```

# Test source

```ts
  447 |     await expect(page.locator('.entity-search-result').first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  448 | 
  449 |     const links = await page.locator('.entity-search-result').all();
  450 |     expect(links.length).toBeGreaterThan(0);
  451 | 
  452 |     const hrefs = await Promise.all(links.slice(0, 20).map(l => l.getAttribute('href')));
  453 |     const randomLinks = getRandomItems(hrefs.filter(Boolean) as string[], 3);
  454 | 
  455 |     for (const href of randomLinks) {
  456 |       const entityResponse = await page.goto(toRelativePath(href));
  457 |       expect(entityResponse?.status(), `${toRelativePath(href)} should answer 200`).toBe(200);
  458 |       await expect(page.locator('h1')).toBeVisible();
  459 |     }
  460 |   });
  461 | 
  462 |   test('counties section - index and random detail pages', async ({ page }) => {
  463 |     await page.goto('/counties/');
  464 | 
  465 |     // Map view is default — verify Leaflet map renders with GeoJSON state shapes
  466 |     await expect(page.locator('#map.leaflet-container')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  467 |     await expect(page.locator('#map .leaflet-interactive').first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  468 |     const shapes = await page.locator('#map .leaflet-interactive').all();
  469 |     expect(shapes.length).toBeGreaterThan(0);
  470 | 
  471 |     // Switch to List view to test county results and detail navigation
  472 |     await page.locator('.toggle-btn[data-view="list"]').click();
  473 | 
  474 |     // Wait for results to load
  475 |     await page.waitForSelector('.county-result, .county-error', { timeout: DATA_LOAD_TIMEOUT });
  476 |     const results = await page.locator('.county-result').all();
  477 |     expect(results.length).toBeGreaterThan(0);
  478 | 
  479 |     // County results are <a> tags - pick random ones and visit detail pages
  480 |     const hrefs = await Promise.all(results.slice(0, 20).map(r => r.getAttribute('href')));
  481 |     const randomLinks = getRandomItems(hrefs.filter(Boolean) as string[], 3);
  482 | 
  483 |     for (const href of randomLinks) {
  484 |       const entityResponse = await page.goto(toRelativePath(href));
  485 |       expect(entityResponse?.status(), `${toRelativePath(href)} should answer 200`).toBe(200);
  486 |       await expect(page.locator('h1')).toBeVisible();
  487 |     }
  488 |   });
  489 | 
  490 |   test('places section - index and random detail pages', async ({ page }) => {
  491 |     await page.goto('/places/');
  492 | 
  493 |     // Map view is default — verify Leaflet map renders with GeoJSON state shapes
  494 |     await expect(page.locator('#map.leaflet-container')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  495 |     await expect(page.locator('#map .leaflet-interactive').first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  496 |     const shapes = await page.locator('#map .leaflet-interactive').all();
  497 |     expect(shapes.length).toBeGreaterThan(0);
  498 | 
  499 |     // Switch to List view to test place results and detail navigation
  500 |     await page.locator('.toggle-btn[data-view="list"]').click();
  501 | 
  502 |     // Wait for results to load
  503 |     await page.waitForSelector('.place-result, .place-error', { timeout: DATA_LOAD_TIMEOUT });
  504 |     const results = await page.locator('.place-result').all();
  505 |     expect(results.length).toBeGreaterThan(0);
  506 | 
  507 |     // Pick random results and visit detail pages
  508 |     const hrefs = await Promise.all(results.slice(0, 20).map(r => r.getAttribute('href')));
  509 |     const randomLinks = getRandomItems(hrefs.filter(Boolean) as string[], 3);
  510 | 
  511 |     for (const href of randomLinks) {
  512 |       const entityResponse = await page.goto(toRelativePath(href));
  513 |       expect(entityResponse?.status(), `${toRelativePath(href)} should answer 200`).toBe(200);
  514 |       await expect(page.locator('h1')).toBeVisible();
  515 |     }
  516 |   });
  517 | 
  518 |   test('postalcodes section - index and random detail pages', async ({ page }) => {
  519 |     await page.goto('/postalcodes/');
  520 | 
  521 |     // Map view is default — verify Leaflet map renders with GeoJSON state shapes
  522 |     await expect(page.locator('#map.leaflet-container')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  523 |     await expect(page.locator('#map .leaflet-interactive').first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  524 |     const shapes = await page.locator('#map .leaflet-interactive').all();
  525 |     expect(shapes.length).toBeGreaterThan(0);
  526 | 
  527 |     // Switch to List view to test postal code results and detail navigation
  528 |     await page.locator('.toggle-btn[data-view="list"]').click();
  529 | 
  530 |     // Wait for results to load
  531 |     await page.waitForSelector('.postal-result, .postal-error', { timeout: DATA_LOAD_TIMEOUT });
  532 |     const results = await page.locator('.postal-result').all();
  533 |     expect(results.length).toBeGreaterThan(0);
  534 | 
  535 |     // Pick random results and visit detail pages
  536 |     const hrefs = await Promise.all(results.slice(0, 20).map(r => r.getAttribute('href')));
  537 |     const randomLinks = getRandomItems(hrefs.filter(Boolean) as string[], 3);
  538 | 
  539 |     for (const href of randomLinks) {
  540 |       const entityResponse = await page.goto(toRelativePath(href));
  541 |       expect(entityResponse?.status(), `${toRelativePath(href)} should answer 200`).toBe(200);
  542 |       await expect(page.locator('h1')).toBeVisible();
  543 |     }
  544 |   });
  545 | 
  546 |   test('precincts section - map view and state filter', async ({ page }) => {
> 547 |     await page.goto('/precincts/');
      |                ^ Error: page.goto: net::ERR_ABORTED; maybe frame was detached?
  548 | 
  549 |     // Map view is default — verify Leaflet map renders with GeoJSON state shapes
  550 |     await expect(page.locator('#map.leaflet-container')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  551 |     await expect(page.locator('#map .leaflet-interactive').first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  552 |     const shapes = await page.locator('#map .leaflet-interactive').all();
  553 |     expect(shapes.length).toBeGreaterThan(0);
  554 | 
  555 |     // Switch to List view to test state filter and precinct results
  556 |     await page.locator('.toggle-btn[data-view="list"]').click();
  557 | 
  558 |     // Select a state first (precincts require state selection)
  559 |     const stateSelect = page.locator('#precinct-state-select');
  560 |     await expect(stateSelect).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  561 |     await stateSelect.selectOption('TX');
  562 | 
  563 |     // Wait for precinct results to load
  564 |     await page.waitForSelector('.precinct-result, .precinct-error', { timeout: DATA_LOAD_TIMEOUT });
  565 |     const results = await page.locator('.precinct-result').all();
  566 |     expect(results.length).toBeGreaterThan(0);
  567 | 
  568 |     // pct_dem_lead is stored as a ratio; browse cards must show percentage points.
  569 |     // Votes and margin are read from the same card in one evaluate: the list can
  570 |     // re-render between separate reads, which compared one card's votes with
  571 |     // another card's margin (flaky in preview-e2e-full-dq8n6, 2026-10-12). Polled,
  572 |     // so a list still settling is retried and a real mismatch still fails.
  573 |     const first = page.locator('.precinct-result').first();
  574 |     await expect.poll(async () => first.evaluate((card) => {
  575 |       const votes = (sel: string) => Number(card.querySelector(sel)?.textContent?.replace(/[^0-9]/g, '') || 0);
  576 |       const dem = votes('.precinct-vote-dem');
  577 |       const rep = votes('.precinct-vote-rep');
  578 |       const total = votes('.precinct-vote-total');
  579 |       const margin = total ? (dem - rep) / total * 100 : 0;
  580 |       const expected = margin === 0 ? 'Tie' : `${margin > 0 ? 'D' : 'R'}+${Math.abs(margin).toFixed(1)}%`;
  581 |       const shown = card.querySelector('.precinct-margin')?.textContent?.trim() ?? '';
  582 |       return shown === expected ? 'consistent' : `card shows ${shown}, votes give ${expected}`;
  583 |     }), { timeout: 10000, message: 'precinct card margin matches its votes' }).toBe('consistent');
  584 | 
  585 |     // Pick random results and visit detail pages
  586 |     const hrefs = await Promise.all(results.slice(0, 20).map(r => r.getAttribute('href')));
  587 |     const validHrefs = hrefs.filter(Boolean) as string[];
  588 | 
  589 |     if (validHrefs.length > 0) {
  590 |       const randomLinks = getRandomItems(validHrefs, 3);
  591 |       for (const href of randomLinks) {
  592 |         const entityResponse = await page.goto(toRelativePath(href));
  593 |         expect(entityResponse?.status(), `${toRelativePath(href)} should answer 200`).toBe(200);
  594 |         await expect(page.locator('h1')).toBeVisible();
  595 |       }
  596 |     }
  597 |   });
  598 | 
  599 |   test('tracts section - state/county filter and random detail pages', async ({ page }) => {
  600 |     test.setTimeout(120000); // Tracts require sequential state→county→tract interactions with GraphQL
  601 |     await page.goto('/tracts/');
  602 | 
  603 |     // Switch to List view — tract-index.js loads dynamically on list view activation
  604 |     await page.locator('.toggle-btn[data-view="list"]').click();
  605 | 
  606 |     // Wait for state dropdown to populate (tract-index.js loads states via GraphQL)
  607 |     const stateSelect = page.locator('#tract-state-select');
  608 |     await page.waitForFunction(() => {
  609 |       const select = document.querySelector('#tract-state-select') as HTMLSelectElement;
  610 |       return select && select.options.length > 1;
  611 |     }, { timeout: 45000 });
  612 | 
  613 |     // Select Texas
  614 |     await stateSelect.selectOption('TX');
  615 | 
  616 |     // Wait for county dropdown to populate
  617 |     const countySelect = page.locator('#tract-county-select');
  618 |     await expect(countySelect).toBeEnabled({ timeout: DATA_LOAD_TIMEOUT });
  619 |     await page.waitForFunction(() => {
  620 |       const select = document.querySelector('#tract-county-select') as HTMLSelectElement;
  621 |       return select && select.options.length > 1;
  622 |     }, { timeout: DATA_LOAD_TIMEOUT });
  623 | 
  624 |     // Select first county
  625 |     const options = await countySelect.locator('option').all();
  626 |     if (options.length > 1) {
  627 |       const value = await options[1].getAttribute('value');
  628 |       if (value) await countySelect.selectOption(value);
  629 |     }
  630 | 
  631 |     // Wait for tract results to load
  632 |     await page.waitForSelector('.tract-result, .tract-error', { timeout: DATA_LOAD_TIMEOUT });
  633 |     const results = await page.locator('.tract-result').all();
  634 |     expect(results.length).toBeGreaterThan(0);
  635 | 
  636 |     // Pick random results and visit detail pages
  637 |     const hrefs = await Promise.all(results.slice(0, 20).map(r => r.getAttribute('href')));
  638 |     const validHrefs = hrefs.filter(Boolean) as string[];
  639 | 
  640 |     if (validHrefs.length > 0) {
  641 |       const randomLinks = getRandomItems(validHrefs, 3);
  642 |       for (const href of randomLinks) {
  643 |         const entityResponse = await page.goto(toRelativePath(href));
  644 |         expect(entityResponse?.status(), `${toRelativePath(href)} should answer 200`).toBe(200);
  645 |         await expect(page.locator('h1')).toBeVisible();
  646 |       }
  647 |     }
```