# 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 - individuals (premium) >> individuals section - index and random pages
- Location: tests/ui/entities.spec.ts:659:7

# Error details

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

Locator: locator('h1')
Timeout: 5000ms
Expected pattern: /Individual/i
Received string:  "
                Sign in to elect.info
            "

Call log:
  - Expect "toContainText" locator('h1') with timeout 5000ms
  - waiting for locator('h1')
    12 × locator resolved to <h1 class="pf-c-title pf-m-3xl ak-m-clamped">…</h1>
       - unexpected value "
                Sign in to elect.info
            "

```

```yaml
- heading "Sign in to elect.info" [level=1]
```

# Test source

```ts
  565 |     expect(results.length).toBeGreaterThan(0);
  566 | 
  567 |     // pct_dem_lead is stored as a ratio; browse cards must show percentage points.
  568 |     // Votes and margin are read from the same card in one evaluate: the list can
  569 |     // re-render between separate reads, which compared one card's votes with
  570 |     // another card's margin (flaky in preview-e2e-full-dq8n6, 2026-10-12). Polled,
  571 |     // so a list still settling is retried and a real mismatch still fails.
  572 |     const first = page.locator('.precinct-result').first();
  573 |     await expect.poll(async () => first.evaluate((card) => {
  574 |       const votes = (sel: string) => Number(card.querySelector(sel)?.textContent?.replace(/[^0-9]/g, '') || 0);
  575 |       const dem = votes('.precinct-vote-dem');
  576 |       const rep = votes('.precinct-vote-rep');
  577 |       const total = votes('.precinct-vote-total');
  578 |       const margin = total ? (dem - rep) / total * 100 : 0;
  579 |       const expected = margin === 0 ? 'Tie' : `${margin > 0 ? 'D' : 'R'}+${Math.abs(margin).toFixed(1)}%`;
  580 |       const shown = card.querySelector('.precinct-margin')?.textContent?.trim() ?? '';
  581 |       return shown === expected ? 'consistent' : `card shows ${shown}, votes give ${expected}`;
  582 |     }), { timeout: 10000, message: 'precinct card margin matches its votes' }).toBe('consistent');
  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, baseURL }) => {
  660 |     test.skip(!hasAuthCredentials, 'Requires PLAYWRIGHT_AUTH_USER/PLAYWRIGHT_AUTH_PASS');
  661 |     await premiumVisitor(page.context(), baseURL);
  662 | 
  663 |     const indexResponse = await page.goto('/individuals/');
  664 |     expect(indexResponse?.status(), '/individuals/ should answer 200').toBe(200);
> 665 |     await expect(page.locator('h1')).toContainText(/Individual/i);
      |                                      ^ Error: expect(locator).toContainText(expected) failed
  666 | 
  667 |     await expect(page.locator('.entity-search-result').first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  668 | 
  669 |     const links = await page.locator('.entity-search-result').all();
  670 |     expect(links.length).toBeGreaterThan(0);
  671 | 
  672 |     const hrefs = await Promise.all(links.slice(0, 20).map(l => l.getAttribute('href')));
  673 |     const randomLinks = getRandomItems(hrefs.filter(Boolean) as string[], 3);
  674 | 
  675 |     for (const href of randomLinks) {
  676 |       const entityResponse = await page.goto(toRelativePath(href));
  677 |       expect(entityResponse?.status(), `${toRelativePath(href)} should answer 200`).toBe(200);
  678 |       // Search hits must carry graph Individual ids: the detail page renders
  679 |       // "Individual Not Found" (still a 200 shell) for any other id.
  680 |       await expect(page.locator('.leaf-detail-page h1')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  681 |       await expect(page.locator('.leaf-detail-page h1')).not.toHaveText(/Not Found/i);
  682 |     }
  683 |   });
  684 | });
  685 | 
```