# 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/routing.spec.ts >> routing: every index link resolves >> /districts/ first-page links all answer 200 or 301
- Location: tests/ui/routing.spec.ts:83:9

# Error details

```
Error: /districts/ should list results

expect(locator).toBeVisible() failed

Locator: locator('.entity-search-result').first()
Expected: visible
Timeout: 15000ms
Error: element(s) not found

Call log:
  - /districts/ should list results locator('.entity-search-result').first() with timeout 15000ms
  - waiting for locator('.entity-search-result').first()

```

```yaml
- link "← Back to Home":
  - /url: /
- banner:
  - heading "Congressional Districts" [level=1]
  - paragraph: US House of Representatives districts
- button "Map"
- button "List"
- main:
  - img
  - button "Zoom in"
  - button "Zoom out"
  - link "Leaflet":
    - /url: https://leafletjs.com
  - text: ©
  - link "OpenStreetMap":
    - /url: https://www.openstreetmap.org/copyright
```

# Test source

```ts
  1   | import { test, expect, APIRequestContext } from '@playwright/test';
  2   | import { canonicalPath, getRandomItems, toRelativePath } from './helpers';
  3   | 
  4   | /*
  5   |  * Routing coverage across every collection (electinfo/rundeck#700, ops#514).
  6   |  *
  7   |  * What slipped past the existing specs:
  8   |  *   - a /tracts/ catch-all location shadowed the alias resolver, so every tract
  9   |  *     URL answered 200 with the JS browse page and no rel=canonical (sites#161)
  10  |  *     -- nothing sampled tracts;
  11  |  *   - the preview nginx lacked the /politicians/ -> /candidates/ rewrite the
  12  |  *     production config has (sites#162) -- only 3 random index links were followed;
  13  |  *   - unknown paths fall through to the Payload slug lookup, which answers
  14  |  *     400/500 instead of 404.
  15  |  */
  16  | 
  17  | const ALIASES = process.env.EXPECT_ALIASES === '1';
  18  | 
  19  | /** Collections whose sitemap lists canonical URLs (rundeck PagePaths Sitemaps + CyclePages). */
  20  | const SITEMAP_COLLECTIONS = ['candidates', 'committees', 'parties', 'offices', 'districts', 'states', 'elections'];
  21  | 
  22  | /** Indexes whose first results page lists entity links (.entity-search-result). */
  23  | // (/states/ lists map chips, covered by its sitemap test instead)
  24  | const LINKED_INDEXES = ['/candidates/', '/committees/', '/parties/', '/offices/', '/districts/', '/elections/', '/employers/', '/vendors/'];
  25  | 
  26  | const locs = (xml: string) => [...xml.matchAll(/<loc>([^<]+)<\/loc>/g)].map(m => m[1]);
  27  | 
  28  | /** Canonical page URLs from a collection's sitemap, following one level of sitemap index. */
  29  | async function sitemapLocs(request: APIRequestContext, collection: string): Promise<string[]> {
  30  |   const path = `/${collection}/sitemap-${collection}.xml`;
  31  |   const res = await request.get(path);
  32  |   expect(res.status(), `${path} should answer 200`).toBe(200);
  33  |   const xml = await res.text();
  34  |   if (!xml.includes('<sitemapindex')) return locs(xml);
  35  |   const [child] = getRandomItems(locs(xml), 1, `child:${path}`);
  36  |   const childPath = toRelativePath(child);
  37  |   const childRes = await request.get(childPath);
  38  |   expect(childRes.status(), `${childPath} should answer 200`).toBe(200);
  39  |   return locs(await childRes.text());
  40  | }
  41  | 
  42  | test.describe('routing: canonical URLs per collection', () => {
  43  |   test.skip(!ALIASES, 'Set EXPECT_ALIASES=1 against a target with published page paths');
  44  | 
  45  |   for (const collection of SITEMAP_COLLECTIONS) {
  46  |     test(`${collection}: sitemap URLs serve their page with rel=canonical = the sitemap URL`, async ({ request }) => {
  47  |       // Entity pages only: a sitemap may also list the collection's index (/elections/)
  48  |       const entityLocs = (await sitemapLocs(request, collection)).filter(l => toRelativePath(l) !== `/${collection}/`);
  49  |       const sample = getRandomItems(entityLocs, 3);
  50  |       expect(sample.length, `${collection} sitemap should list URLs`).toBeGreaterThan(0);
  51  |       for (const loc of sample) {
  52  |         const path = toRelativePath(loc);
  53  |         const res = await request.get(path, { maxRedirects: 0 });
  54  |         expect(res.status(), `${path} (from the ${collection} sitemap) should answer 200`).toBe(200);
  55  |         expect(canonicalPath(await res.text()), `${path} rel=canonical`).toBe(path);
  56  |       }
  57  |     });
  58  |   }
  59  | 
  60  |   test('tract URLs resolve through the alias resolver, not a catch-all', async ({ request }) => {
  61  |     // Regression for sites#161: Travis County tract 48453000101 by GEOID
  62  |     const res = await request.get('/tracts/48453000101/', { maxRedirects: 0 });
  63  |     expect([200, 301], '/tracts/48453000101/ should answer 200 or 301').toContain(res.status());
  64  |     if (res.status() === 200) {
  65  |       expect(canonicalPath(await res.text()), '/tracts/48453000101/ rel=canonical').toMatch(/^\/tracts\/[^/]+\/$/);
  66  |     }
  67  |   });
  68  | 
  69  |   test('party badge links by FEC code redirect to the party page', async ({ request }) => {
  70  |     // us.elect.news links parties as /parties/<code>/ (sites#164)
  71  |     for (const code of ['rep', 'dem']) {
  72  |       const res = await request.get(`/parties/${code}/`, { maxRedirects: 0 });
  73  |       expect(res.status(), `/parties/${code}/ should redirect permanently`).toBe(301);
  74  |       const target = res.headers()['location'] ?? '';
  75  |       const page = await request.get(target, { maxRedirects: 0 });
  76  |       expect(page.status(), `/parties/${code}/ -> ${target} should answer 200`).toBe(200);
  77  |     }
  78  |   });
  79  | });
  80  | 
  81  | test.describe('routing: every index link resolves', () => {
  82  |   for (const index of LINKED_INDEXES) {
  83  |     test(`${index} first-page links all answer 200 or 301`, async ({ page, request }) => {
  84  |       test.setTimeout(120000);
  85  |       const res = await page.goto(index);
  86  |       expect(res?.status(), `${index} should answer 200`).toBe(200);
> 87  |       await expect(page.locator('.entity-search-result').first(), `${index} should list results`).toBeVisible({ timeout: 15000 });
      |                                                                                                   ^ Error: /districts/ should list results
  88  |       const hrefs = (await page.locator('.entity-search-result').evaluateAll(els => els.map(e => e.getAttribute('href'))))
  89  |         .filter((h): h is string => !!h)
  90  |         .map(toRelativePath);
  91  |       const broken: string[] = [];
  92  |       for (const href of [...new Set(hrefs)]) {
  93  |         const r = await request.get(href, { maxRedirects: 0 });
  94  |         if (r.status() !== 200 && r.status() !== 301) broken.push(`${href} -> ${r.status()}`);
  95  |       }
  96  |       expect(broken, `${index} links that do not resolve`).toEqual([]);
  97  |     });
  98  |   }
  99  | });
  100 | 
  101 | test.describe('routing: unknown addresses', () => {
  102 |   test('an unknown entity path answers 404', async ({ request }) => {
  103 |     // Today the miss falls through to Payload /api/slug-lookup, which answers
  104 |     // 400/500. Remove test.fail once slug-lookup returns 404 (ops#514 follow-up).
  105 |     test.fail(true, 'Payload slug-lookup answers 400/500 for unknown paths');
  106 |     for (const path of ['/candidates/no-such-entity-zz/', '/tracts/no-such-tract/']) {
  107 |       const res = await request.get(path, { maxRedirects: 0 });
  108 |       expect(res.status(), `${path} should answer 404`).toBe(404);
  109 |     }
  110 |   });
  111 | });
  112 | 
```