# 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:548:7

# Error details

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

Locator:  locator('.precinct-result').first().locator('.precinct-margin')
Expected: "R+40.4%"
Received: "R+31.8%"
Timeout:  5000ms

Call log:
  - Expect "toHaveText" locator('.precinct-result').first().locator('.precinct-margin') with timeout 5000ms
  - waiting for locator('.precinct-result').first().locator('.precinct-margin')
    14 × locator resolved to <span class="precinct-margin rep-lead">R+31.8%</span>
       - unexpected value "R+31.8%"

```

```yaml
- text: R+31.8%
```

# Test source

```ts
  482 |     const hrefs = await Promise.all(results.slice(0, 20).map(r => r.getAttribute('href')));
  483 |     const randomLinks = getRandomItems(hrefs.filter(Boolean) as string[], 3);
  484 | 
  485 |     for (const href of randomLinks) {
  486 |       const entityResponse = await page.goto(toRelativePath(href));
  487 |       expect(entityResponse?.status(), `${toRelativePath(href)} should answer 200`).toBe(200);
  488 |       await expect(page.locator('h1')).toBeVisible();
  489 |     }
  490 |   });
  491 | 
  492 |   test('places section - index and random detail pages', async ({ page }) => {
  493 |     await page.goto('/places/');
  494 | 
  495 |     // Map view is default — verify Leaflet map renders with GeoJSON state shapes
  496 |     await expect(page.locator('#map.leaflet-container')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  497 |     await expect(page.locator('#map .leaflet-interactive').first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  498 |     const shapes = await page.locator('#map .leaflet-interactive').all();
  499 |     expect(shapes.length).toBeGreaterThan(0);
  500 | 
  501 |     // Switch to List view to test place results and detail navigation
  502 |     await page.locator('.toggle-btn[data-view="list"]').click();
  503 | 
  504 |     // Wait for results to load
  505 |     await page.waitForSelector('.place-result, .place-error', { timeout: DATA_LOAD_TIMEOUT });
  506 |     const results = await page.locator('.place-result').all();
  507 |     expect(results.length).toBeGreaterThan(0);
  508 | 
  509 |     // Pick random results and visit detail pages
  510 |     const hrefs = await Promise.all(results.slice(0, 20).map(r => r.getAttribute('href')));
  511 |     const randomLinks = getRandomItems(hrefs.filter(Boolean) as string[], 3);
  512 | 
  513 |     for (const href of randomLinks) {
  514 |       const entityResponse = await page.goto(toRelativePath(href));
  515 |       expect(entityResponse?.status(), `${toRelativePath(href)} should answer 200`).toBe(200);
  516 |       await expect(page.locator('h1')).toBeVisible();
  517 |     }
  518 |   });
  519 | 
  520 |   test('postalcodes section - index and random detail pages', async ({ page }) => {
  521 |     await page.goto('/postalcodes/');
  522 | 
  523 |     // Map view is default — verify Leaflet map renders with GeoJSON state shapes
  524 |     await expect(page.locator('#map.leaflet-container')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  525 |     await expect(page.locator('#map .leaflet-interactive').first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  526 |     const shapes = await page.locator('#map .leaflet-interactive').all();
  527 |     expect(shapes.length).toBeGreaterThan(0);
  528 | 
  529 |     // Switch to List view to test postal code results and detail navigation
  530 |     await page.locator('.toggle-btn[data-view="list"]').click();
  531 | 
  532 |     // Wait for results to load
  533 |     await page.waitForSelector('.postal-result, .postal-error', { timeout: DATA_LOAD_TIMEOUT });
  534 |     const results = await page.locator('.postal-result').all();
  535 |     expect(results.length).toBeGreaterThan(0);
  536 | 
  537 |     // Pick random results and visit detail pages
  538 |     const hrefs = await Promise.all(results.slice(0, 20).map(r => r.getAttribute('href')));
  539 |     const randomLinks = getRandomItems(hrefs.filter(Boolean) as string[], 3);
  540 | 
  541 |     for (const href of randomLinks) {
  542 |       const entityResponse = await page.goto(toRelativePath(href));
  543 |       expect(entityResponse?.status(), `${toRelativePath(href)} should answer 200`).toBe(200);
  544 |       await expect(page.locator('h1')).toBeVisible();
  545 |     }
  546 |   });
  547 | 
  548 |   test('precincts section - map view and state filter', async ({ page }) => {
  549 |     await page.goto('/precincts/');
  550 | 
  551 |     // Map view is default — verify Leaflet map renders with GeoJSON state shapes
  552 |     await expect(page.locator('#map.leaflet-container')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  553 |     await expect(page.locator('#map .leaflet-interactive').first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  554 |     const shapes = await page.locator('#map .leaflet-interactive').all();
  555 |     expect(shapes.length).toBeGreaterThan(0);
  556 | 
  557 |     // Switch to List view to test state filter and precinct results
  558 |     await page.locator('.toggle-btn[data-view="list"]').click();
  559 | 
  560 |     // Select a state first (precincts require state selection)
  561 |     const stateSelect = page.locator('#precinct-state-select');
  562 |     await expect(stateSelect).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  563 |     await stateSelect.selectOption('TX');
  564 | 
  565 |     // Wait for precinct results to load
  566 |     await page.waitForSelector('.precinct-result, .precinct-error', { timeout: DATA_LOAD_TIMEOUT });
  567 |     const results = await page.locator('.precinct-result').all();
  568 |     expect(results.length).toBeGreaterThan(0);
  569 | 
  570 |     // pct_dem_lead is stored as a ratio; browse cards must show percentage points.
  571 |     const first = page.locator('.precinct-result').first();
  572 |     const parseVotes = async (selector: string) => Number(
  573 |       (await first.locator(selector).textContent())?.replace(/[^0-9]/g, '') || 0
  574 |     );
  575 |     const votesDem = await parseVotes('.precinct-vote-dem');
  576 |     const votesRep = await parseVotes('.precinct-vote-rep');
  577 |     const votesTotal = await parseVotes('.precinct-vote-total');
  578 |     const expectedMargin = votesTotal ? (votesDem - votesRep) / votesTotal * 100 : 0;
  579 |     const expectedLabel = expectedMargin === 0
  580 |       ? 'Tie'
  581 |       : `${expectedMargin > 0 ? 'D' : 'R'}+${Math.abs(expectedMargin).toFixed(1)}%`;
> 582 |     await expect(first.locator('.precinct-margin')).toHaveText(expectedLabel);
      |                                                     ^ Error: expect(locator).toHaveText(expected) failed
  583 | 
  584 |     // Pick random results and visit detail pages
  585 |     const hrefs = await Promise.all(results.slice(0, 20).map(r => r.getAttribute('href')));
  586 |     const validHrefs = hrefs.filter(Boolean) as string[];
  587 | 
  588 |     if (validHrefs.length > 0) {
  589 |       const randomLinks = getRandomItems(validHrefs, 3);
  590 |       for (const href of randomLinks) {
  591 |         const entityResponse = await page.goto(toRelativePath(href));
  592 |         expect(entityResponse?.status(), `${toRelativePath(href)} should answer 200`).toBe(200);
  593 |         await expect(page.locator('h1')).toBeVisible();
  594 |       }
  595 |     }
  596 |   });
  597 | 
  598 |   test('tracts section - state/county filter and random detail pages', async ({ page }) => {
  599 |     test.setTimeout(120000); // Tracts require sequential state→county→tract interactions with GraphQL
  600 |     await page.goto('/tracts/');
  601 | 
  602 |     // Switch to List view — tract-index.js loads dynamically on list view activation
  603 |     await page.locator('.toggle-btn[data-view="list"]').click();
  604 | 
  605 |     // Wait for state dropdown to populate (tract-index.js loads states via GraphQL)
  606 |     const stateSelect = page.locator('#tract-state-select');
  607 |     await page.waitForFunction(() => {
  608 |       const select = document.querySelector('#tract-state-select') as HTMLSelectElement;
  609 |       return select && select.options.length > 1;
  610 |     }, { timeout: 45000 });
  611 | 
  612 |     // Select Texas
  613 |     await stateSelect.selectOption('TX');
  614 | 
  615 |     // Wait for county dropdown to populate
  616 |     const countySelect = page.locator('#tract-county-select');
  617 |     await expect(countySelect).toBeEnabled({ timeout: DATA_LOAD_TIMEOUT });
  618 |     await page.waitForFunction(() => {
  619 |       const select = document.querySelector('#tract-county-select') as HTMLSelectElement;
  620 |       return select && select.options.length > 1;
  621 |     }, { timeout: DATA_LOAD_TIMEOUT });
  622 | 
  623 |     // Select first county
  624 |     const options = await countySelect.locator('option').all();
  625 |     if (options.length > 1) {
  626 |       const value = await options[1].getAttribute('value');
  627 |       if (value) await countySelect.selectOption(value);
  628 |     }
  629 | 
  630 |     // Wait for tract results to load
  631 |     await page.waitForSelector('.tract-result, .tract-error', { timeout: DATA_LOAD_TIMEOUT });
  632 |     const results = await page.locator('.tract-result').all();
  633 |     expect(results.length).toBeGreaterThan(0);
  634 | 
  635 |     // Pick random results and visit detail pages
  636 |     const hrefs = await Promise.all(results.slice(0, 20).map(r => r.getAttribute('href')));
  637 |     const validHrefs = hrefs.filter(Boolean) as string[];
  638 | 
  639 |     if (validHrefs.length > 0) {
  640 |       const randomLinks = getRandomItems(validHrefs, 3);
  641 |       for (const href of randomLinks) {
  642 |         const entityResponse = await page.goto(toRelativePath(href));
  643 |         expect(entityResponse?.status(), `${toRelativePath(href)} should answer 200`).toBe(200);
  644 |         await expect(page.locator('h1')).toBeVisible();
  645 |       }
  646 |     }
  647 |   });
  648 | 
  649 | });
  650 | 
  651 | test.describe('us.elect.info entity sections - individuals (premium)', () => {
  652 |   test.use({
  653 |     httpCredentials: hasAuthCredentials ? {
  654 |       username: process.env.PLAYWRIGHT_AUTH_USER!,
  655 |       password: process.env.PLAYWRIGHT_AUTH_PASS || '',
  656 |     } : undefined
  657 |   });
  658 | 
  659 |   test('individuals section - index and random pages', async ({ page }) => {
  660 |     test.skip(!hasAuthCredentials, 'Requires PLAYWRIGHT_AUTH_USER/PLAYWRIGHT_AUTH_PASS');
  661 | 
  662 |     const indexResponse = await page.goto('/individuals/');
  663 |     expect(indexResponse?.status(), '/individuals/ should answer 200').toBe(200);
  664 |     await expect(page.locator('h1')).toContainText(/Individual/i);
  665 | 
  666 |     await expect(page.locator('.entity-search-result').first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  667 | 
  668 |     const links = await page.locator('.entity-search-result').all();
  669 |     expect(links.length).toBeGreaterThan(0);
  670 | 
  671 |     const hrefs = await Promise.all(links.slice(0, 20).map(l => l.getAttribute('href')));
  672 |     const randomLinks = getRandomItems(hrefs.filter(Boolean) as string[], 3);
  673 | 
  674 |     for (const href of randomLinks) {
  675 |       const entityResponse = await page.goto(toRelativePath(href));
  676 |       expect(entityResponse?.status(), `${toRelativePath(href)} should answer 200`).toBe(200);
  677 |       await expect(page.locator('h1')).toBeVisible();
  678 |     }
  679 |   });
  680 | });
  681 | 
```