# 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/entity-graph.spec.ts >> entity-graph script injection >> not injected on /elections/2024/
- Location: tests/ui/entity-graph.spec.ts:44:9

# Error details

```
Error: apiRequestContext.get: connect ECONNREFUSED 10.1.105.22:80
Call log:
  - → GET http://10.1.105.22/elections/2024/
    - user-agent: Playwright/1.64.1 (x64; ubuntu 22.04) node/20.9 CI/1
    - accept: */*
    - accept-encoding: gzip,deflate,br

```

# Test source

```ts
  1   | import { test, expect } from '@playwright/test';
  2   | import { visitorHeaders } from './helpers';
  3   | 
  4   | // Entity graph pop-over (electinfo/sites#249). nginx injects /js/entity-graph.js
  5   | // next to money-flow.js on Candidate and Committee detail pages. The script
  6   | // adds a "Graph" button to the header; opening it fetches entityGraph from
  7   | // /api/graphql, loads /js/entity-graph-view.js (Cytoscape) and draws the node
  8   | // and its most significant neighbours in a modal dialog.
  9   | //
  10  | // Same targets and pages as money-flow.spec.ts.
  11  | 
  12  | const SCRIPT = '<script src="/js/entity-graph.js" defer></script>';
  13  | const DNC = '/committees/a75a9110-f1db-a945-39a8-7503ed3c04da/'; // C00010603
  14  | const HARRIS = '/candidates/a8897257-430c-2bf3-4498-349021b09ca3/'; // P00009423
  15  | const OTHER_PAGES = [
  16  |   '/elections/2024/',
  17  |   '/offices/president/',
  18  | ];
  19  | // Geography pages carry the graph by graph id (us-pages GraphScript, nodeGraph).
  20  | const AK_ID = '032f8d61-f554-f435-2a6d-50b3c8cc01d7';
  21  | const AK = `/states/${AK_ID}/`;
  22  | const LA_COUNTY = '92472335-c8ae-c198-6366-9eb734141e61';
  23  | const LA_TRACT = '/tracts/0078f0bb-c0ad-4639-13fc-a29547adddac/';
  24  | const NODE_QUERY = 'query($l: String!, $i: String!) { nodeGraph(label: $l, id: $i) { focalId nodes { id label focal displayName } edges { type } groups { type label shown total } } }';
  25  | 
  26  | test.describe('entity-graph script injection', () => {
  27  |   for (const path of [DNC, HARRIS]) {
  28  |     test(`injected once on ${path}`, async ({ request }) => {
  29  |       const res = await request.get(path);
  30  |       expect(res.status()).toBe(200);
  31  |       expect((await res.text()).split(SCRIPT).length - 1).toBe(1);
  32  |     });
  33  |   }
  34  | 
  35  |   test(`a geography page names its graph node: ${AK}`, async ({ request }) => {
  36  |     const res = await request.get(AK);
  37  |     expect(res.status()).toBe(200);
  38  |     const html = await res.text();
  39  |     test.skip(!html.includes('entity-graph.js'), 'the us-pages renderer predates GraphScript');
  40  |     expect(html).toMatch(/<script src="\/js\/entity-graph\.js" defer(="")? data-graph-label="State" data-graph-id="[0-9a-f-]{36}">/);
  41  |   });
  42  | 
  43  |   for (const path of OTHER_PAGES) {
  44  |     test(`not injected on ${path}`, async ({ request }) => {
> 45  |       const res = await request.get(path);
      |                                 ^ Error: apiRequestContext.get: connect ECONNREFUSED 10.1.105.22:80
  46  |       expect(res.status()).toBe(200);
  47  |       expect(await res.text()).not.toContain('entity-graph.js');
  48  |     });
  49  |   }
  50  | });
  51  | 
  52  | test.describe('entityGraph API', () => {
  53  |   for (const fecId of ['C00010603', 'P00009423']) {
  54  |     test(`${fecId}: neighbours without individuals, money edges with amounts`, async ({ request }) => {
  55  |       const res = await request.post('/api/graphql', {
  56  |         data: {
  57  |           query: 'query($f: String!) { entityGraph(fecId: $f) { focalId nodes { id label labels focal displayName } edges { type amount } } }',
  58  |           variables: { f: fecId },
  59  |         },
  60  |         headers: visitorHeaders(),
  61  |         timeout: 60000,
  62  |       });
  63  |       expect(res.status()).toBe(200);
  64  |       const g = (await res.json())?.data?.entityGraph;
  65  |       expect(g).toBeTruthy();
  66  |       expect(g.nodes.filter((n: { focal: boolean }) => n.focal)).toHaveLength(1);
  67  |       expect(g.nodes.length).toBeGreaterThan(5);
  68  |       expect(g.nodes.some((n: { labels: string[] }) => n.labels.includes('Individual'))).toBe(false);
  69  |       expect(g.nodes.every((n: { displayName: string }) => !!n.displayName)).toBe(true);
  70  |       expect(g.edges.some((e: { amount: number | null }) => e.amount != null && e.amount > 0)).toBe(true);
  71  |     });
  72  |   }
  73  | });
  74  | 
  75  | /** nodeGraph, or null where the API predates it (a CI pod against an older API): the caller skips. */
  76  | async function nodeGraph(request: import('@playwright/test').APIRequestContext, label: string, id: string) {
  77  |   const res = await request.post('/api/graphql', {
  78  |     data: { query: NODE_QUERY, variables: { l: label, i: id } },
  79  |     headers: visitorHeaders(),
  80  |     timeout: 60000,
  81  |   });
  82  |   expect(res.status()).toBe(200);
  83  |   const body = await res.json();
  84  |   if (body?.errors?.some((e: { message: string }) => /Cannot query field "nodeGraph"/.test(e.message))) return { missing: true as const };
  85  |   return { missing: false as const, body };
  86  | }
  87  | 
  88  | test.describe('nodeGraph API', () => {
  89  |   test('a county: neighbours drawn, blocks counted, no individuals', async ({ request }) => {
  90  |     const r = await nodeGraph(request, 'County', LA_COUNTY);
  91  |     test.skip(r.missing, 'the API has no nodeGraph yet');
  92  |     const g = r.body?.data?.nodeGraph;
  93  |     expect(g).toBeTruthy();
  94  |     expect(g.nodes.filter((n: { focal: boolean }) => n.focal)).toHaveLength(1);
  95  |     expect(g.nodes.length).toBeGreaterThan(10);
  96  |     expect(g.nodes.some((n: { label: string }) => n.label === 'Individual' || n.label === 'Block')).toBe(false);
  97  |     const blocks = g.groups.find((x: { label: string }) => x.label === 'Block');
  98  |     expect(blocks.shown).toBe(0);
  99  |     expect(blocks.total).toBeGreaterThan(10000);
  100 |     expect(g.edges.some((e: { type: string }) => e.type === 'LOCATED_IN')).toBe(true);
  101 |   });
  102 | 
  103 |   test('an unknown label is null', async ({ request }) => {
  104 |     const r = await nodeGraph(request, 'Committee', LA_COUNTY);
  105 |     test.skip(r.missing, 'the API has no nodeGraph yet');
  106 |     expect(r.body?.data?.nodeGraph).toBeNull();
  107 |   });
  108 | 
  109 |   test('voterGraph is refused on the public endpoint', async ({ request }) => {
  110 |     const res = await request.post('/api/graphql', {
  111 |       data: { query: 'query($i: String!) { voterGraph(id: $i) { focalId } }', variables: { i: 'x' } },
  112 |       timeout: 30000,
  113 |     });
  114 |     const body = await res.json().catch(() => null);
  115 |     test.skip(!!body?.errors?.some((e: { message: string }) => /Cannot query field "voterGraph"/.test(e.message)), 'the API has no voterGraph yet');
  116 |     // the visitor-token gate (an unlisted document) or the API's voters scope
  117 |     expect(res.status() >= 400 || body?.errors?.length > 0).toBe(true);
  118 |     expect(body?.data?.voterGraph ?? null).toBeNull();
  119 |   });
  120 | });
  121 | 
  122 | /**
  123 |  * Where the host has accounts the graph is a member control in the identity bar
  124 |  * (sites#355), locked for anonymous visitors (preview's e2e run is anonymous);
  125 |  * the drawing tests sign in a mocked member. hosts without accounts keep the
  126 |  * header button (last test).
  127 |  */
  128 | async function asMember(page: import('@playwright/test').Page): Promise<void> {
  129 |   await page.route('**/auth/whoami*', r => r.fulfill({ json: { enabled: 'true', email: 'e2e-member@example.org', elevated: false, entitlements: {} } }));
  130 |   await page.route('**/auth/account/me*', r => r.fulfill({ json: { requests: [], agreement: null, granted: [] } }));
  131 | }
  132 | 
  133 | test.describe('entity-graph pop-over', () => {
  134 |   for (const path of [DNC, HARRIS]) {
  135 |     test(`opens and draws on ${path}`, async ({ page }) => {
  136 |       const errors: string[] = [];
  137 |       page.on('pageerror', (e) => errors.push(String(e)));
  138 |       await asMember(page);
  139 |       await page.goto(path);
  140 |       const button = page.locator('#auth-slot [data-control="graph"]');
  141 |       await expect(button).toHaveCount(1, { timeout: 15000 });
  142 |       await expect(button).not.toHaveAttribute('aria-disabled', 'true');
  143 |       await button.click();
  144 |       const dialog = page.locator('dialog.eg-dialog');
  145 |       await expect(dialog).toBeVisible();
```