# 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 >> /parties/ first-page links all answer 200 or 301
- Location: tests/ui/routing.spec.ts:73:9

# Error details

```
Error: /parties/ links that do not resolve

expect(received).toEqual(expected) // deep equality

- Expected  -  1
+ Received  + 48

- Array []
+ Array [
+   "/parties/78a61b65-dba1-0686-97bb-bdde7e1944a9/ -> 503",
+   "/parties/5dbee893-48fe-1da0-2259-b84f2a0c98c2/ -> 503",
+   "/parties/e17300bf-3fec-5895-c05e-461443a32a60/ -> 503",
+   "/parties/7d03bab7-e4a2-bdb1-ce16-952161c13084/ -> 503",
+   "/parties/e04d121d-fc56-a04d-84b7-7daa6a59fe4c/ -> 503",
+   "/parties/daeb4657-d625-b0e8-643f-c471b2ec39fe/ -> 503",
+   "/parties/beffbefb-6eaa-d428-3aae-1fbe8ddf0eae/ -> 503",
+   "/parties/6d9df45c-0287-de76-c16c-5df8a3cd797e/ -> 503",
+   "/parties/4b94bb92-6d21-e869-c21f-48e36501c46e/ -> 503",
+   "/parties/f93836bb-d946-079d-7dbb-b593f71006b5/ -> 503",
+   "/parties/d85f9b21-18ec-b69e-100c-7012e68cc86d/ -> 503",
+   "/parties/aaf05808-47a6-74be-40ab-d1ff8af359d0/ -> 503",
+   "/parties/c6279a7f-3917-7b3d-d12a-fb889cd1f482/ -> 503",
+   "/parties/412e2507-6b3c-2d47-6fcc-7f2fe6f7aeb0/ -> 503",
+   "/parties/d5722feb-e0d6-20f7-d53a-a45c5f602cc7/ -> 503",
+   "/parties/be2bbde4-d6b8-52d8-1913-0489d711d647/ -> 503",
+   "/parties/bc760549-18f0-3c03-6285-6177d81d0c51/ -> 503",
+   "/parties/0e890051-768a-7d89-e270-f89ec801e156/ -> 503",
+   "/parties/750c1ba0-6af9-e84e-36e0-1fe8485dbf85/ -> 503",
+   "/parties/89842f65-d4bb-8318-e929-8f9ccd3da177/ -> 503",
+   "/parties/1599b155-8a42-6124-31f9-d2e9b33316eb/ -> 503",
+   "/parties/90b0c6c3-51ff-6d65-0a8a-98699aca94bc/ -> 503",
+   "/parties/96227b21-69de-b30b-10eb-3a6fd0b4c60d/ -> 503",
+   "/parties/35754e53-3aac-0fca-6a85-768423c0c4b4/ -> 503",
+   "/parties/9b51c006-e5c9-fcf7-59e9-ff355c9283b9/ -> 503",
+   "/parties/6dc47509-f4f3-11bd-3ce2-876372272ff3/ -> 503",
+   "/parties/6ab4d54b-82ad-eeed-3a24-34db57e9e6a4/ -> 503",
+   "/parties/d0a0a7ea-da44-cdbd-9ee6-13dbd292a023/ -> 503",
+   "/parties/fc4e412f-920b-f8c6-29f9-5025d1513817/ -> 503",
+   "/parties/98ab48a1-0521-eb8f-a0a9-2ebc93c165f0/ -> 503",
+   "/parties/a76d0376-77b6-f7b3-8e28-4e7e324befef/ -> 503",
+   "/parties/0d835f05-068e-2842-9b27-37ee4006580d/ -> 503",
+   "/parties/aff25f10-5d75-539b-d0b1-b8509dff8ea6/ -> 503",
+   "/parties/b5b68da8-d111-597b-6101-91c61a076656/ -> 503",
+   "/parties/e713a335-c9b7-42b1-d08d-9e6e1d33ed8b/ -> 503",
+   "/parties/fcbe763a-6f0d-f676-9b71-ea126d2784d2/ -> 503",
+   "/parties/763de19e-9fcc-ecb8-7b45-60a3cb7d5b89/ -> 503",
+   "/parties/d5d1234c-539f-b01a-f5fc-3e7fc0cf5571/ -> 503",
+   "/parties/d0b9cf86-74a1-968f-ef13-75051dc1f38b/ -> 503",
+   "/parties/664fbf7b-ac46-c571-4991-8b5f0fe72aba/ -> 503",
+   "/parties/5535f721-e1b1-95c7-c528-81f0be40672d/ -> 503",
+   "/parties/b6c7bfe6-dc23-4188-196d-eb5064a82d37/ -> 503",
+   "/parties/db7e0f97-63c4-aca2-cc92-f4e0d5687b8e/ -> 503",
+   "/parties/b9d4ca16-396e-b304-706c-ec38e117ca5b/ -> 503",
+   "/parties/420a5851-5f81-2b42-5be7-613d407db6d2/ -> 503",
+   "/parties/3d8fedf4-b281-4484-d88e-234f33e92922/ -> 503",
+ ]
```

# Page snapshot

```yaml
- generic [active] [ref=e1]:
  - link "← Back to Home" [ref=e2] [cursor=pointer]:
    - /url: /
  - banner [ref=e3]:
    - heading "Political Parties" [level=1] [ref=e4]
    - paragraph [ref=e5]: Party affiliations in US federal elections
  - generic [ref=e6]:
    - generic [ref=e7]:
      - generic [ref=e8]: Not signed in
      - generic [ref=e10]:
        - link "Log in" [ref=e11] [cursor=pointer]:
          - /url: /outpost.goauthentik.io/start?rd=http%3A%2F%2Fpreview-us-elect-info.electinfo.svc.cluster.local%2Fparties%2F
        - link "Create an account" [ref=e12] [cursor=pointer]:
          - /url: /signin/register?rd=http%3A%2F%2Fpreview-us-elect-info.electinfo.svc.cluster.local%2Fparties%2F
    - toolbar "Member tools" [ref=e13]:
      - button "All cycles (Sign in to use the cycle facet)" [disabled] [ref=e14] [cursor=pointer]:
        - generic [ref=e18]: All cycles
      - button "Graph (Sign in to use Graph)" [disabled] [ref=e21] [cursor=pointer]:
        - generic [ref=e30]: Graph
  - generic [ref=e33]:
    - generic [ref=e34]:
      - generic [ref=e35]:
        - combobox "Search entities" [ref=e36]
        - status [ref=e37]
      - button "Search" [ref=e38] [cursor=pointer]
    - generic [ref=e39]:
      - heading "All Parties" [level=3] [ref=e41]
      - generic [ref=e42]:
        - link "Republican Party REP - 20,575 candidates" [ref=e43] [cursor=pointer]:
          - /url: /parties/78a61b65-dba1-0686-97bb-bdde7e1944a9/
          - generic [ref=e44]: Republican Party
          - generic [ref=e45]: REP - 20,575 candidates
        - link "Democratic Party DEM - 19,645 candidates" [ref=e46] [cursor=pointer]:
          - /url: /parties/12cac40d-b40d-abcc-6735-5e49f3925192/
          - generic [ref=e47]: Democratic Party
          - generic [ref=e48]: DEM - 19,645 candidates
        - link "Independent IND - 4,666 candidates" [ref=e49] [cursor=pointer]:
          - /url: /parties/b4c3baa2-547f-8f59-13d4-ab4e352f9904/
          - generic [ref=e50]: Independent
          - generic [ref=e51]: IND - 4,666 candidates
        - link "Libertarian Party LIB - 3,150 candidates" [ref=e52] [cursor=pointer]:
          - /url: /parties/5dbee893-48fe-1da0-2259-b84f2a0c98c2/
          - generic [ref=e53]: Libertarian Party
          - generic [ref=e54]: LIB - 3,150 candidates
        - link "Other OTH - 2,060 candidates" [ref=e55] [cursor=pointer]:
          - /url: /parties/e17300bf-3fec-5895-c05e-461443a32a60/
          - generic [ref=e56]: Other
          - generic [ref=e57]: OTH - 2,060 candidates
        - link "Unknown UNK - 1,375 candidates" [ref=e58] [cursor=pointer]:
          - /url: /parties/7d03bab7-e4a2-bdb1-ce16-952161c13084/
          - generic [ref=e59]: Unknown
          - generic [ref=e60]: UNK - 1,375 candidates
        - link "Green Party GRE - 763 candidates" [ref=e61] [cursor=pointer]:
          - /url: /parties/e04d121d-fc56-a04d-84b7-7daa6a59fe4c/
          - generic [ref=e62]: Green Party
          - generic [ref=e63]: GRE - 763 candidates
        - link "NNE NNE - 684 candidates" [ref=e64] [cursor=pointer]:
          - /url: /parties/daeb4657-d625-b0e8-643f-c471b2ec39fe/
          - generic [ref=e65]: NNE
          - generic [ref=e66]: NNE - 684 candidates
        - link "No Party Affiliation NPA - 395 candidates" [ref=e67] [cursor=pointer]:
          - /url: /parties/f2bd1d9a-d441-7e6b-3fe2-e3c31607f89e/
          - generic [ref=e68]: No Party Affiliation
          - generic [ref=e69]: NPA - 395 candidates
        - link "Natural Law Party NLP - 313 candidates" [ref=e70] [cursor=pointer]:
          - /url: /parties/ad7637d8-9076-a056-749e-78444e72d926/
          - generic [ref=e71]: Natural Law Party
          - generic [ref=e72]: NLP - 313 candidates
        - link "UN UN - 301 candidates" [ref=e73] [cursor=pointer]:
          - /url: /parties/beffbefb-6eaa-d428-3aae-1fbe8ddf0eae/
          - generic [ref=e74]: UN
          - generic [ref=e75]: UN - 301 candidates
        - link "Democratic-Farmer-Labor DFL - 281 candidates" [ref=e76] [cursor=pointer]:
          - /url: /parties/6d9df45c-0287-de76-c16c-5df8a3cd797e/
          - generic [ref=e77]: Democratic-Farmer-Labor
          - generic [ref=e78]: DFL - 281 candidates
        - link "Reform Party REF - 226 candidates" [ref=e79] [cursor=pointer]:
          - /url: /parties/4b94bb92-6d21-e869-c21f-48e36501c46e/
          - generic [ref=e80]: Reform Party
          - generic [ref=e81]: REF - 226 candidates
        - link "Socialist Workers Party SWP - 198 candidates" [ref=e82] [cursor=pointer]:
          - /url: /parties/f93836bb-d946-079d-7dbb-b593f71006b5/
          - generic [ref=e83]: Socialist Workers Party
          - generic [ref=e84]: SWP - 198 candidates
        - link "Conservative Party CRV - 176 candidates" [ref=e85] [cursor=pointer]:
          - /url: /parties/d85f9b21-18ec-b69e-100c-7012e68cc86d/
          - generic [ref=e86]: Conservative Party
          - generic [ref=e87]: CRV - 176 candidates
        - link "American Independent Party AIP - 149 candidates" [ref=e88] [cursor=pointer]:
          - /url: /parties/aaf05808-47a6-74be-40ab-d1ff8af359d0/
          - generic [ref=e89]: American Independent Party
          - generic [ref=e90]: AIP - 149 candidates
        - link "Peace and Freedom PAF - 146 candidates" [ref=e91] [cursor=pointer]:
          - /url: /parties/c6279a7f-3917-7b3d-d12a-fb889cd1f482/
          - generic [ref=e92]: Peace and Freedom
          - generic [ref=e93]: PAF - 146 candidates
        - link "Constitution Party CON - 131 candidates" [ref=e94] [cursor=pointer]:
          - /url: /parties/412e2507-6b3c-2d47-6fcc-7f2fe6f7aeb0/
          - generic [ref=e95]: Constitution Party
          - generic [ref=e96]: CON - 131 candidates
        - link "Non-Party NON - 120 candidates" [ref=e97] [cursor=pointer]:
          - /url: /parties/d5722feb-e0d6-20f7-d53a-a45c5f602cc7/
          - generic [ref=e98]: Non-Party
          - generic [ref=e99]: NON - 120 candidates
        - link "IDP IDP - 114 candidates" [ref=e100] [cursor=pointer]:
          - /url: /parties/be2bbde4-d6b8-52d8-1913-0489d711d647/
          - generic [ref=e101]: IDP
          - generic [ref=e102]: IDP - 114 candidates
        - link "Right to Life RTL - 112 candidates" [ref=e103] [cursor=pointer]:
          - /url: /parties/bc760549-18f0-3c03-6285-6177d81d0c51/
          - generic [ref=e104]: Right to Life
          - generic [ref=e105]: RTL - 112 candidates
        - link "Constitutional CST - 102 candidates" [ref=e106] [cursor=pointer]:
          - /url: /parties/0e890051-768a-7d89-e270-f89ec801e156/
          - generic [ref=e107]: Constitutional
          - generic [ref=e108]: CST - 102 candidates
        - link "Independent Conserv. Democratic ICD - 98 candidates" [ref=e109] [cursor=pointer]:
          - /url: /parties/750c1ba0-6af9-e84e-36e0-1fe8485dbf85/
          - generic [ref=e110]: Independent Conserv. Democratic
          - generic [ref=e111]: ICD - 98 candidates
        - link "Independent American Party IAP - 83 candidates" [ref=e112] [cursor=pointer]:
          - /url: /parties/89842f65-d4bb-8318-e929-8f9ccd3da177/
          - generic [ref=e113]: Independent American Party
          - generic [ref=e114]: IAP - 83 candidates
        - link "Taxpayers TX - 68 candidates" [ref=e115] [cursor=pointer]:
          - /url: /parties/1599b155-8a42-6124-31f9-d2e9b33316eb/
          - generic [ref=e116]: Taxpayers
          - generic [ref=e117]: TX - 68 candidates
        - link "Communist Party COM - 65 candidates" [ref=e118] [cursor=pointer]:
          - /url: /parties/90b0c6c3-51ff-6d65-0a8a-98699aca94bc/
          - generic [ref=e119]: Communist Party
          - generic [ref=e120]: COM - 65 candidates
        - link "American Party AMP - 59 candidates" [ref=e121] [cursor=pointer]:
          - /url: /parties/96227b21-69de-b30b-10eb-3a6fd0b4c60d/
          - generic [ref=e122]: American Party
          - generic [ref=e123]: AMP - 59 candidates
        - link "NOP NOP - 58 candidates" [ref=e124] [cursor=pointer]:
          - /url: /parties/35754e53-3aac-0fca-6a85-768423c0c4b4/
          - generic [ref=e125]: NOP
          - generic [ref=e126]: NOP - 58 candidates
        - link "Liberal Party LBL - 50 candidates" [ref=e127] [cursor=pointer]:
          - /url: /parties/9b51c006-e5c9-fcf7-59e9-ff355c9283b9/
          - generic [ref=e128]: Liberal Party
          - generic [ref=e129]: LBL - 50 candidates
        - link "Socialist Party SOC - 48 candidates" [ref=e130] [cursor=pointer]:
          - /url: /parties/6dc47509-f4f3-11bd-3ce2-876372272ff3/
          - generic [ref=e131]: Socialist Party
          - generic [ref=e132]: SOC - 48 candidates
        - link "Citizens' Party CIT - 43 candidates" [ref=e133] [cursor=pointer]:
          - /url: /parties/6ab4d54b-82ad-eeed-3a24-34db57e9e6a4/
          - generic [ref=e134]: Citizens' Party
          - generic [ref=e135]: CIT - 43 candidates
        - link "Veterans Party VET - 26 candidates" [ref=e136] [cursor=pointer]:
          - /url: /parties/d0a0a7ea-da44-cdbd-9ee6-13dbd292a023/
          - generic [ref=e137]: Veterans Party
          - generic [ref=e138]: VET - 26 candidates
        - link "Federalist FED - 21 candidates" [ref=e139] [cursor=pointer]:
          - /url: /parties/fc4e412f-920b-f8c6-29f9-5025d1513817/
          - generic [ref=e140]: Federalist
          - generic [ref=e141]: FED - 21 candidates
        - link "U.S. Taxpayers Party UST - 21 candidates" [ref=e142] [cursor=pointer]:
          - /url: /parties/98ab48a1-0521-eb8f-a0a9-2ebc93c165f0/
          - generic [ref=e143]: U.S. Taxpayers Party
          - generic [ref=e144]: UST - 21 candidates
        - link "American Independent Conservative AIC - 19 candidates" [ref=e145] [cursor=pointer]:
          - /url: /parties/a76d0376-77b6-f7b3-8e28-4e7e324befef/
          - generic [ref=e146]: American Independent Conservative
          - generic [ref=e147]: AIC - 19 candidates
        - link "Populist Party POP - 19 candidates" [ref=e148] [cursor=pointer]:
          - /url: /parties/0d835f05-068e-2842-9b27-37ee4006580d/
          - generic [ref=e149]: Populist Party
          - generic [ref=e150]: POP - 19 candidates
        - link "GOP GOP - 18 candidates" [ref=e151] [cursor=pointer]:
          - /url: /parties/aff25f10-5d75-539b-d0b1-b8509dff8ea6/
          - generic [ref=e152]: GOP
          - generic [ref=e153]: GOP - 18 candidates
        - link "Progressive Party PRO - 18 candidates" [ref=e154] [cursor=pointer]:
          - /url: /parties/b5b68da8-d111-597b-6101-91c61a076656/
          - generic [ref=e155]: Progressive Party
          - generic [ref=e156]: PRO - 18 candidates
        - link "Peace and Freedom Party PFP - 17 candidates" [ref=e157] [cursor=pointer]:
          - /url: /parties/e713a335-c9b7-42b1-d08d-9e6e1d33ed8b/
          - generic [ref=e158]: Peace and Freedom Party
          - generic [ref=e159]: PFP - 17 candidates
        - link "American People's Freedom Party APF - 16 candidates" [ref=e160] [cursor=pointer]:
          - /url: /parties/fcbe763a-6f0d-f676-9b71-ea126d2784d2/
          - generic [ref=e161]: American People's Freedom Party
          - generic [ref=e162]: APF - 16 candidates
        - link "Labor Party LAB - 15 candidates" [ref=e163] [cursor=pointer]:
          - /url: /parties/763de19e-9fcc-ecb8-7b45-60a3cb7d5b89/
          - generic [ref=e164]: Labor Party
          - generic [ref=e165]: LAB - 15 candidates
        - link "UNI UNI - 15 candidates" [ref=e166] [cursor=pointer]:
          - /url: /parties/d5d1234c-539f-b01a-f5fc-3e7fc0cf5571/
          - generic [ref=e167]: UNI
          - generic [ref=e168]: UNI - 15 candidates
        - link "Ace Party ACE - 14 candidates" [ref=e169] [cursor=pointer]:
          - /url: /parties/d0b9cf86-74a1-968f-ef13-75051dc1f38b/
          - generic [ref=e170]: Ace Party
          - generic [ref=e171]: ACE - 14 candidates
        - link "NPP NPP - 14 candidates" [ref=e172] [cursor=pointer]:
          - /url: /parties/664fbf7b-ac46-c571-4991-8b5f0fe72aba/
          - generic [ref=e173]: NPP
          - generic [ref=e174]: NPP - 14 candidates
        - link "Human Rights Party HRP - 13 candidates" [ref=e175] [cursor=pointer]:
          - /url: /parties/5535f721-e1b1-95c7-c528-81f0be40672d/
          - generic [ref=e176]: Human Rights Party
          - generic [ref=e177]: HRP - 13 candidates
        - link "Prohibition Party NAP - 13 candidates" [ref=e178] [cursor=pointer]:
          - /url: /parties/b6c7bfe6-dc23-4188-196d-eb5064a82d37/
          - generic [ref=e179]: Prohibition Party
          - generic [ref=e180]: NAP - 13 candidates
        - link "SLP SLP - 13 candidates" [ref=e181] [cursor=pointer]:
          - /url: /parties/db7e0f97-63c4-aca2-cc92-f4e0d5687b8e/
          - generic [ref=e182]: SLP
          - generic [ref=e183]: SLP - 13 candidates
        - link "CNC CNC - 12 candidates" [ref=e184] [cursor=pointer]:
          - /url: /parties/b9d4ca16-396e-b304-706c-ec38e117ca5b/
          - generic [ref=e185]: CNC
          - generic [ref=e186]: CNC - 12 candidates
        - link "N/A N/A - 10 candidates" [ref=e187] [cursor=pointer]:
          - /url: /parties/420a5851-5f81-2b42-5be7-613d407db6d2/
          - generic [ref=e188]: N/A
          - generic [ref=e189]: N/A - 10 candidates
        - link "Alaskan Independence Party AKI - 9 candidates" [ref=e190] [cursor=pointer]:
          - /url: /parties/3d8fedf4-b281-4484-d88e-234f33e92922/
          - generic [ref=e191]: Alaskan Independence Party
          - generic [ref=e192]: AKI - 9 candidates
      - generic [ref=e193]:
        - generic [ref=e194]: Showing 1-50
        - generic [ref=e195]:
          - button "First" [disabled] [ref=e196]
          - button "Prev" [disabled] [ref=e197]
          - button "1" [ref=e198] [cursor=pointer]
          - button "Next" [ref=e199] [cursor=pointer]
  - main [ref=e200]
```

# Test source

```ts
  1   | import { test, expect, APIRequestContext } from '@playwright/test';
  2   | import { canonicalPath, getRandomItems, premiumGated, 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   |  *   - the preview nginx lacked the /politicians/ -> /candidates/ rewrite the
  9   |  *     production config has (sites#162) -- only 3 random index links were followed;
  10  |  *   - unknown paths fall through to the Payload slug lookup, which answers
  11  |  *     400/500 instead of 404.
  12  |  */
  13  | 
  14  | const ALIASES = process.env.EXPECT_ALIASES === '1';
  15  | 
  16  | /** Collections whose sitemap lists canonical URLs (rundeck PagePaths Sitemaps + CyclePages). */
  17  | const SITEMAP_COLLECTIONS = ['candidates', 'committees', 'parties', 'offices', 'districts', 'states', 'elections'];
  18  | 
  19  | /** Indexes whose first results page lists entity links (.entity-search-result). */
  20  | // (/states/ lists map chips — its sitemap test covers it; /districts/ uses the
  21  | // shared EntityIndex and lists .entity-search-result directly, so it is included here)
  22  | const LINKED_INDEXES = ['/candidates/', '/committees/', '/parties/', '/offices/', '/districts/', '/elections/', '/employers/', '/vendors/'];
  23  | 
  24  | const locs = (xml: string) => [...xml.matchAll(/<loc>([^<]+)<\/loc>/g)].map(m => m[1]);
  25  | 
  26  | /** Canonical page URLs from a collection's sitemap, following one level of sitemap index. */
  27  | async function sitemapLocs(request: APIRequestContext, collection: string): Promise<string[]> {
  28  |   const path = `/${collection}/sitemap-${collection}.xml`;
  29  |   const res = await request.get(path);
  30  |   expect(res.status(), `${path} should answer 200`).toBe(200);
  31  |   const xml = await res.text();
  32  |   if (!xml.includes('<sitemapindex')) return locs(xml);
  33  |   const [child] = getRandomItems(locs(xml), 1, `child:${path}`);
  34  |   const childPath = toRelativePath(child);
  35  |   const childRes = await request.get(childPath);
  36  |   expect(childRes.status(), `${childPath} should answer 200`).toBe(200);
  37  |   return locs(await childRes.text());
  38  | }
  39  | 
  40  | test.describe('routing: canonical URLs per collection', () => {
  41  |   test.skip(!ALIASES, 'Set EXPECT_ALIASES=1 against a target with published page paths');
  42  | 
  43  |   for (const collection of SITEMAP_COLLECTIONS) {
  44  |     test(`${collection}: sitemap URLs serve their page with rel=canonical = the sitemap URL`, async ({ request }) => {
  45  |       // Entity pages only: a sitemap may also list the collection's index (/elections/)
  46  |       const entityLocs = (await sitemapLocs(request, collection)).filter(l => toRelativePath(l) !== `/${collection}/`);
  47  |       const sample = getRandomItems(entityLocs, 3);
  48  |       expect(sample.length, `${collection} sitemap should list URLs`).toBeGreaterThan(0);
  49  |       for (const loc of sample) {
  50  |         const path = toRelativePath(loc);
  51  |         const res = await request.get(path, { maxRedirects: 0 });
  52  |         expect(res.status(), `${path} (from the ${collection} sitemap) should answer 200`).toBe(200);
  53  |         expect(canonicalPath(await res.text()), `${path} rel=canonical`).toBe(path);
  54  |       }
  55  |     });
  56  |   }
  57  | 
  58  | 
  59  |   test('party badge links by FEC code redirect to the party page', async ({ request }) => {
  60  |     // us.elect.news links parties as /parties/<code>/ (sites#164)
  61  |     for (const code of ['rep', 'dem']) {
  62  |       const res = await request.get(`/parties/${code}/`, { maxRedirects: 0 });
  63  |       expect(res.status(), `/parties/${code}/ should redirect permanently`).toBe(301);
  64  |       const target = res.headers()['location'] ?? '';
  65  |       const page = await request.get(target, { maxRedirects: 0 });
  66  |       expect(page.status(), `/parties/${code}/ -> ${target} should answer 200`).toBe(200);
  67  |     }
  68  |   });
  69  | });
  70  | 
  71  | test.describe('routing: every index link resolves', () => {
  72  |   for (const index of LINKED_INDEXES) {
  73  |     test(`${index} first-page links all answer 200 or 301`, async ({ page, request }) => {
  74  |       test.setTimeout(120000);
  75  |       const res = await page.goto(index);
  76  |       expect(res?.status(), `${index} should answer 200`).toBe(200);
  77  |       await expect(page.locator('.entity-search-result').first(), `${index} should list results`).toBeVisible({ timeout: 15000 });
  78  |       const hrefs = (await page.locator('.entity-search-result').evaluateAll(els => els.map(e => e.getAttribute('href'))))
  79  |         .filter((h): h is string => !!h)
  80  |         .map(toRelativePath);
  81  |       const broken: string[] = [];
  82  |       for (const href of [...new Set(hrefs)]) {
  83  |         const r = await request.get(href, { maxRedirects: 0 });
  84  |         if (r.status() !== 200 && r.status() !== 301) broken.push(`${href} -> ${r.status()}`);
  85  |       }
> 86  |       expect(broken, `${index} links that do not resolve`).toEqual([]);
      |                                                            ^ Error: /parties/ links that do not resolve
  87  |     });
  88  |   }
  89  | });
  90  | 
  91  | test.describe('routing: unknown addresses', () => {
  92  |   test('an unknown entity path answers 404', async ({ request }) => {
  93  |     // A miss in /_resolve (or the API being unavailable) ends in nginx's own
  94  |     // 404. It used to fall through to Payload's slug-lookup, which answered
  95  |     // 400/500 (electinfo/sites#218). Only the alias-routed collections apply:
  96  |     // leaf collections are covered below.
  97  |     for (const path of ['/candidates/no-such-entity-zz/', '/committees/no-such-entity-zz/']) {
  98  |       const res = await request.get(path, { maxRedirects: 0 });
  99  |       expect(res.status(), `${path} should answer 404`).toBe(404);
  100 |     }
  101 |   });
  102 | 
  103 |   test('an unknown leaf entity path serves the collection shell', async ({ request }) => {
  104 |     // Leaf collections (individuals, employers, vendors, blocks, precincts,
  105 |     // tracts) render client-side from the baked collection shell (sites#209),
  106 |     // so nginx answers every subpath with that shell and the runtime reports
  107 |     // a missing entity itself. There is no server-side 404 to expect here.
  108 |     for (const col of ['individuals', 'employers', 'vendors', 'blocks', 'precincts', 'tracts']) {
  109 |       // A premium section closed to this visitor answers sign-in or 403 instead (auth.spec.ts).
  110 |       if (await premiumGated(request, `/${col}/`)) continue;
  111 |       const res = await request.get(`/${col}/no-such-entity-zz/`, { maxRedirects: 0 });
  112 |       expect(res.status(), `/${col}/no-such-entity-zz/ should serve the shell`).toBe(200);
  113 |       expect(res.headers()['content-type'] ?? '', `/${col}/ shell is HTML`).toContain('text/html');
  114 |     }
  115 |   });
  116 | });
  117 | 
```