# 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/serving.spec.ts >> rendered candidate and committee pages (us-pages) >> a House candidate links its party, office and district
- Location: tests/ui/serving.spec.ts:120:7

# Error details

```
Error: apiRequestContext.get: connect ECONNREFUSED 10.1.105.22:80
Call log:
  - → GET http://10.1.105.22/candidates/bfcafdd4-62da-f107-22fb-ff5413c33fa4/
    - user-agent: Playwright/1.64.1 (x64; ubuntu 22.04) node/20.9 CI/1
    - accept: */*
    - accept-encoding: gzip,deflate,br

```

# Test source

```ts
  21  | 
  22  | test.describe('sitemaps', () => {
  23  |   for (const collection of ['candidates', 'committees']) {
  24  |     test(`${collection}: the index lists one file per lead character, each a urlset`, async ({ request }) => {
  25  |       const res = await request.get(`/${collection}/sitemap-${collection}.xml`);
  26  |       expect(res.status()).toBe(200);
  27  |       expect(res.headers()['content-type']).toContain('xml');
  28  |       const xml = await res.text();
  29  |       expect(xml).toContain('<sitemapindex');
  30  |       const children = locs(xml).map(toRelativePath);
  31  |       expect(children).toEqual(LEAD_BUCKETS.map((b) => `/${collection}/sitemap-${collection}-${b}.xml`));
  32  |       for (const child of [children[1], children[children.length - 1]] as string[]) {
  33  |         const c = await request.get(child);
  34  |         expect(c.status(), child).toBe(200);
  35  |         const body = await c.text();
  36  |         expect(body).toContain('<urlset');
  37  |         for (const loc of locs(body)) expect(toRelativePath(loc)).toMatch(new RegExp(`^/${collection}/[^/]+/$`));
  38  |       }
  39  |     });
  40  |   }
  41  | 
  42  |   test('a single-file collection sitemap is a urlset of its own pages', async ({ request }) => {
  43  |     const res = await request.get('/parties/sitemap-parties.xml');
  44  |     expect(res.status()).toBe(200);
  45  |     const all = locs(await res.text());
  46  |     expect(all.length).toBeGreaterThan(50);
  47  |     for (const loc of all) expect(toRelativePath(loc)).toMatch(/^\/parties\/[^/]+\/$/);
  48  |   });
  49  | 
  50  |   test('a sitemap that does not exist is 404', async ({ request }) => {
  51  |     for (const path of ['/candidates/sitemap-nope.xml', '/candidates/sitemap-candidates-zz.xml']) {
  52  |       expect((await request.get(path)).status(), path).toBe(404);
  53  |     }
  54  |   });
  55  | });
  56  | 
  57  | test.describe('static assets', () => {
  58  |   test('state images are SVG with CORS and caching headers', async ({ request }) => {
  59  |     const res = await request.get('/images/states/ak.svg');
  60  |     expect(res.status()).toBe(200);
  61  |     const h = res.headers();
  62  |     expect(h['content-type']).toContain('image/svg+xml');
  63  |     expect(h['access-control-allow-origin']).toBe('*');
  64  |     expect(h['cache-control']).toContain('max-age=');
  65  |     expect(await res.text()).toContain('<svg');
  66  |   });
  67  | 
  68  |   test('district GeoJSON is application/geo+json and parses', async ({ request }) => {
  69  |     const res = await request.get('/geo/districts/al/al-01.geojson');
  70  |     expect(res.status()).toBe(200);
  71  |     expect(res.headers()['content-type']).toContain('application/geo+json');
  72  |     const geo = await res.json();
  73  |     expect(['FeatureCollection', 'Feature']).toContain(geo.type);
  74  |   });
  75  | 
  76  |   test('a missing asset is 404', async ({ request }) => {
  77  |     for (const path of ['/images/states/zz.svg', '/geo/districts/zz/zz-99.geojson']) {
  78  |       expect((await request.get(path)).status(), path).toBe(404);
  79  |     }
  80  |   });
  81  | });
  82  | 
  83  | test.describe('collection roots', () => {
  84  |   test('a collection without its trailing slash redirects relatively to the index page', async ({ request }) => {
  85  |     for (const collection of ['candidates', 'committees', 'parties']) {
  86  |       const res = await request.get(`/${collection}`, { maxRedirects: 0 });
  87  |       expect(res.status(), `/${collection}`).toBe(301);
  88  |       expect(res.headers()['location'], `/${collection} Location`).toBe(`/${collection}/`);
  89  |       expect((await request.get(`/${collection}/`)).status(), `/${collection}/`).toBe(200);
  90  |     }
  91  |   });
  92  | });
  93  | 
  94  | test.describe('rendered candidate and committee pages (us-pages)', () => {
  95  |   test('a candidate page is rendered for the current graph build', async ({ request }) => {
  96  |     const res = await request.get(`/candidates/${BIDEN}/`);
  97  |     expect(res.status()).toBe(200);
  98  |     expect(res.headers()['x-graph-build'], 'the renderer stamps its build key').toBeTruthy();
  99  |     const html = await res.text();
  100 |     expect(canonicalPath(html)).toMatch(/^\/candidates\/[a-z0-9-]+-p80000722\/$/);
  101 |     expect(sectionIds(html).slice(0, 3)).toEqual(['profile', 'timeline', 'funding']);
  102 |     expect(html).not.toContain('Supporting Committees'); // rundeck#779
  103 |     expect(count(html, '<script src="/js/money-flow.js" defer></script>')).toBe(1);
  104 |     expect(count(html, '<script src="/js/entity-graph.js" defer></script>')).toBe(1);
  105 |     const generated = /Generated (\d{4}-\d{2}-\d{2})/.exec(html)?.[1];
  106 |     expect(generated, 'footer date').toBeTruthy();
  107 |     expect(Date.now() - Date.parse(generated as string), 'rendered within the cache lifetime').toBeLessThan(4 * 86_400_000);
  108 |     const rows = (/<section id="funding">[\s\S]*?<\/section>/.exec(html)?.[0] ?? '').match(/<tr>/g)?.length ?? 0;
  109 |     expect(rows - 1, 'Top Contributors rows (minus the header)').toBeGreaterThan(0);
  110 |     expect(rows - 1).toBeLessThanOrEqual(10);
  111 |   });
  112 | 
  113 |   test('/politicians/<id>/ is the candidate page', async ({ request }) => {
  114 |     const a = await request.get(`/candidates/${BIDEN}/`);
  115 |     const b = await request.get(`/politicians/${BIDEN}/`);
  116 |     expect(b.status()).toBe(200);
  117 |     expect(canonicalPath(await b.text())).toBe(canonicalPath(await a.text()));
  118 |   });
  119 | 
  120 |   test('a House candidate links its party, office and district', async ({ request }) => {
> 121 |     const res = await request.get(`/candidates/${PELOSI}/`);
      |                               ^ Error: apiRequestContext.get: connect ECONNREFUSED 10.1.105.22:80
  122 |     expect(res.status()).toBe(200);
  123 |     const html = await res.text();
  124 |     for (const [label, collection] of [
  125 |       ['Party', 'parties'],
  126 |       ['Office', 'offices'],
  127 |       ['District', 'districts'],
  128 |     ]) {
  129 |       expect(html, `${label} link`).toMatch(new RegExp(`<dt>${label}</dt><dd><a href="[^"]*/${collection}/[^"]+/">`));
  130 |     }
  131 |   });
  132 | 
  133 |   test('a committee page lists its money and its party affinity', async ({ request }) => {
  134 |     const res = await request.get(`/committees/${DNC}/`);
  135 |     expect(res.status()).toBe(200);
  136 |     const html = await res.text();
  137 |     expect(canonicalPath(html)).toMatch(/^\/committees\/[a-z0-9-]+-c00010603\/$/);
  138 |     const ids = sectionIds(html);
  139 |     expect(ids.slice(0, 5)).toEqual(['overview', 'financials', 'connected', 'supported', 'funded']);
  140 |     expect(ids.slice(-2)).toEqual(['outgoing', 'incoming']);
  141 |     expect(html).toContain('<dt>Party Affinity</dt>');
  142 |     for (const id of ['funded', 'outgoing', 'incoming']) {
  143 |       const section = new RegExp(`<section id="${id}">[\\s\\S]*?</section>`).exec(html)?.[0] ?? '';
  144 |       const items = section.match(/<li>/g)?.length ?? 0;
  145 |       expect(items, `#${id} items`).toBeLessThanOrEqual(20);
  146 |     }
  147 |   });
  148 | 
  149 |   test('an upper-case path renders with the lower-case canonical', async ({ request }) => {
  150 |     const res = await request.get(`/CANDIDATES/${BIDEN.toUpperCase()}/`, { maxRedirects: 0 });
  151 |     expect(res.status()).toBe(200);
  152 |     expect(canonicalPath(await res.text())).toMatch(/^\/candidates\/[a-z0-9-]+-p80000722\/$/);
  153 |   });
  154 | 
  155 |   test('unknown candidate, committee and party paths are 404 without a static fallback', async ({ request }) => {
  156 |     for (const path of ['/candidates/no-such-entity-zz/', '/committees/no-such-entity-zz/', '/parties/no-such-party-zz/']) {
  157 |       const started = Date.now();
  158 |       const res = await request.get(path, { maxRedirects: 0 });
  159 |       expect(res.status(), path).toBe(404);
  160 |       expect(Date.now() - started, `${path} answers promptly`).toBeLessThan(5000);
  161 |     }
  162 |   });
  163 | 
  164 |   test('the money-flow Sankey and the Graph button work on a rendered candidate page', async ({ page }) => {
  165 |     const errors: string[] = [];
  166 |     page.on('pageerror', (e) => errors.push(String(e)));
  167 |     await page.goto(`/candidates/${PELOSI}/`);
  168 |     await expect(page.locator('h1')).toContainText('PELOSI');
  169 |     await expect(page.locator('#funding .money-flow-chart canvas')).toHaveCount(1, { timeout: 15000 });
  170 |     // The header button without accounts, the identity bar's Graph control with them (sites#355).
  171 |     await expect(page.locator('button.eg-open, #auth-slot [data-control="graph"]')).toHaveCount(1, { timeout: 15000 });
  172 |     expect(errors).toEqual([]);
  173 |   });
  174 | });
  175 | 
```