# 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 pop-over >> opens and draws on /committees/a75a9110-f1db-a945-39a8-7503ed3c04da/
- Location: tests/ui/entity-graph.spec.ts:75:9

# Error details

```
Test timeout of 30000ms exceeded.
```

```
Error: page.goto: net::ERR_ABORTED; maybe frame was detached?
Call log:
  - navigating to "http://10.1.105.75/committees/a75a9110-f1db-a945-39a8-7503ed3c04da/", waiting until "load"

```

# 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  |   '/states/032f8d61-f554-f435-2a6d-50b3c8cc01d7/', // AK
  17  |   '/elections/2024/',
  18  |   '/offices/president/',
  19  | ];
  20  | 
  21  | test.describe('entity-graph script injection', () => {
  22  |   for (const path of [DNC, HARRIS]) {
  23  |     test(`injected once on ${path}`, async ({ request }) => {
  24  |       const res = await request.get(path);
  25  |       expect(res.status()).toBe(200);
  26  |       expect((await res.text()).split(SCRIPT).length - 1).toBe(1);
  27  |     });
  28  |   }
  29  | 
  30  |   for (const path of OTHER_PAGES) {
  31  |     test(`not injected on ${path}`, async ({ request }) => {
  32  |       const res = await request.get(path);
  33  |       expect(res.status()).toBe(200);
  34  |       expect(await res.text()).not.toContain('entity-graph.js');
  35  |     });
  36  |   }
  37  | });
  38  | 
  39  | test.describe('entityGraph API', () => {
  40  |   for (const fecId of ['C00010603', 'P00009423']) {
  41  |     test(`${fecId}: neighbours without individuals, money edges with amounts`, async ({ request }) => {
  42  |       const res = await request.post('/api/graphql', {
  43  |         data: {
  44  |           query: 'query($f: String!) { entityGraph(fecId: $f) { focalId nodes { id label labels focal displayName } edges { type amount } } }',
  45  |           variables: { f: fecId },
  46  |         },
  47  |         headers: visitorHeaders(),
  48  |         timeout: 60000,
  49  |       });
  50  |       expect(res.status()).toBe(200);
  51  |       const g = (await res.json())?.data?.entityGraph;
  52  |       expect(g).toBeTruthy();
  53  |       expect(g.nodes.filter((n: { focal: boolean }) => n.focal)).toHaveLength(1);
  54  |       expect(g.nodes.length).toBeGreaterThan(5);
  55  |       expect(g.nodes.some((n: { labels: string[] }) => n.labels.includes('Individual'))).toBe(false);
  56  |       expect(g.nodes.every((n: { displayName: string }) => !!n.displayName)).toBe(true);
  57  |       expect(g.edges.some((e: { amount: number | null }) => e.amount != null && e.amount > 0)).toBe(true);
  58  |     });
  59  |   }
  60  | });
  61  | 
  62  | /**
  63  |  * Where the host has accounts the graph is a member control in the identity bar
  64  |  * (sites#355), locked for anonymous visitors (preview's e2e run is anonymous);
  65  |  * the drawing tests sign in a mocked member. hosts without accounts keep the
  66  |  * header button (last test).
  67  |  */
  68  | async function asMember(page: import('@playwright/test').Page): Promise<void> {
  69  |   await page.route('**/auth/whoami*', r => r.fulfill({ json: { enabled: 'true', email: 'e2e-member@example.org', elevated: false, entitlements: {} } }));
  70  |   await page.route('**/auth/account/me*', r => r.fulfill({ json: { requests: [], agreement: null, granted: [] } }));
  71  | }
  72  | 
  73  | test.describe('entity-graph pop-over', () => {
  74  |   for (const path of [DNC, HARRIS]) {
  75  |     test(`opens and draws on ${path}`, async ({ page }) => {
  76  |       const errors: string[] = [];
  77  |       page.on('pageerror', (e) => errors.push(String(e)));
  78  |       await asMember(page);
> 79  |       await page.goto(path);
      |                  ^ Error: page.goto: net::ERR_ABORTED; maybe frame was detached?
  80  |       const button = page.locator('#auth-slot [data-control="graph"]');
  81  |       await expect(button).toHaveCount(1, { timeout: 15000 });
  82  |       await expect(button).not.toHaveAttribute('aria-disabled', 'true');
  83  |       await button.click();
  84  |       const dialog = page.locator('dialog.eg-dialog');
  85  |       await expect(dialog).toBeVisible();
  86  |       await expect(dialog.locator('.eg-canvas canvas').first()).toBeVisible({ timeout: 60000 });
  87  |       // centred in the viewport, horizontally and vertically
  88  |       const box = (await dialog.boundingBox())!;
  89  |       const vp = page.viewportSize()!;
  90  |       expect(Math.abs(box.x - (vp.width - box.x - box.width))).toBeLessThanOrEqual(2);
  91  |       expect(Math.abs(box.y - (vp.height - box.y - box.height))).toBeLessThanOrEqual(2);
  92  |       await page.keyboard.press('Escape');
  93  |       await expect(dialog).toBeHidden();
  94  |       expect(errors).toEqual([]);
  95  |     });
  96  |   }
  97  | });
  98  | 
  99  | test.describe('entity-graph without accounts', () => {
  100 |   test('a host without Authentik keeps the header Graph button', async ({ page }) => {
  101 |     await page.route('**/auth/whoami*', r => r.fulfill({ json: { enabled: 'false', email: '', elevated: false, entitlements: {} } }));
  102 |     await page.goto(DNC);
  103 |     await expect(page.locator('button.eg-open')).toHaveCount(1, { timeout: 15000 });
  104 |     await expect(page.locator('#auth-slot [data-control="graph"]')).toHaveCount(0);
  105 |   });
  106 | });
  107 | 
```