# 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:64:9

# Error details

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

```
Error: locator.click: Test timeout of 30000ms exceeded.
Call log:
  - waiting for locator('button.eg-open, #auth-slot [data-control="graph"]')
    - locator resolved to <button type="button" data-control="graph" aria-disabled="true" title="Sign in to use Graph" class="ei-control ei-control--locked" aria-label="Graph (Sign in to use Graph)">…</button>
  - attempting click action
    2 × waiting for element to be visible, enabled and stable
      - element is not enabled
    - retrying click action
    - waiting 20ms
    2 × waiting for element to be visible, enabled and stable
      - element is not enabled
    - retrying click action
      - waiting 100ms
    58 × waiting for element to be visible, enabled and stable
       - element is not enabled
     - retrying click action
       - waiting 500ms

```

# Page snapshot

```yaml
- generic [active] [ref=e1]:
  - banner [ref=e2]:
    - heading "DNC SERVICES CORP / DEMOCRATIC NATIONAL COMMITTEE" [level=1] [ref=e3]
    - paragraph [ref=e4]: Party - Qualified
    - paragraph [ref=e5]: "FEC ID: C00010603"
  - 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%2Fcommittees%2Fa75a9110-f1db-a945-39a8-7503ed3c04da%2F
        - link "Create an account" [ref=e12] [cursor=pointer]:
          - /url: /signin/register?rd=http%3A%2F%2Fpreview-us-elect-info.electinfo.svc.cluster.local%2Fcommittees%2Fa75a9110-f1db-a945-39a8-7503ed3c04da%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]:
    - heading "Overview" [level=2] [ref=e34]
    - generic [ref=e35]:
      - term [ref=e36]: Committee Type
      - definition [ref=e37]: Party - Qualified
      - term [ref=e38]: Designation
      - definition [ref=e39]: U
      - term [ref=e40]: Party Affiliation
      - definition [ref=e41]:
        - link "Democratic Party" [ref=e42] [cursor=pointer]:
          - /url: https://preview.elect.info/parties/democratic/
      - term [ref=e43]: Treasurer
      - definition [ref=e44]: MCGREGOR, VIRGINIA
      - term [ref=e45]: Location
      - definition [ref=e46]:
        - generic [ref=e47]:
          - text: WASHINGTON,
          - link "District of Columbia" [ref=e48] [cursor=pointer]:
            - /url: https://preview.elect.info/states/district-of-columbia/
          - text: "20003"
      - term [ref=e49]: Active Cycles
      - definition [ref=e50]:
        - generic [ref=e51]:
          - link "2026" [ref=e52] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2026/
          - text: ","
          - link "2024" [ref=e53] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2024/
          - text: ","
          - link "2022" [ref=e54] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2022/
          - text: ","
          - link "2020" [ref=e55] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2020/
          - text: ","
          - link "2018" [ref=e56] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2018/
          - text: ","
          - link "2016" [ref=e57] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2016/
          - text: ","
          - link "2014" [ref=e58] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2014/
          - text: ","
          - link "2012" [ref=e59] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2012/
          - text: ","
          - link "2010" [ref=e60] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2010/
          - text: ","
          - link "2008" [ref=e61] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2008/
          - text: ","
          - link "2006" [ref=e62] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2006/
          - text: ","
          - link "2004" [ref=e63] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2004/
          - text: ","
          - link "2002" [ref=e64] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2002/
          - text: ","
          - link "2000" [ref=e65] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2000/
          - text: ","
          - link "1998" [ref=e66] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1998/
          - text: ","
          - link "1996" [ref=e67] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1996/
          - text: ","
          - link "1994" [ref=e68] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1994/
          - text: ","
          - link "1992" [ref=e69] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1992/
          - text: ","
          - link "1990" [ref=e70] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1990/
          - text: ","
          - link "1988" [ref=e71] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1988/
          - text: ","
          - link "1986" [ref=e72] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1986/
          - text: ","
          - link "1984" [ref=e73] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1984/
          - text: ","
          - link "1982" [ref=e74] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1982/
          - text: ","
          - link "1980" [ref=e75] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1980/
      - term [ref=e76]: Party Affinity
      - definition [ref=e77]:
        - generic [ref=e78]:
          - link "Democratic Party" [ref=e79] [cursor=pointer]:
            - /url: https://preview.elect.info/parties/democratic/
          - text: ": 81%"
          - link "Republican Party" [ref=e80] [cursor=pointer]:
            - /url: https://preview.elect.info/parties/republican/
          - text: ": 19%"
  - generic [ref=e81]:
    - heading "Financial Summary" [level=2] [ref=e82]
    - generic [ref=e83]:
      - term [ref=e84]: Total Receipts
      - definition [ref=e85]:
        - strong [ref=e86]: $4505.2M
      - term [ref=e87]: Total Disbursements
      - definition [ref=e88]: $4489.7M
  - generic [ref=e89]:
    - heading "Connected Organizations" [level=2] [ref=e90]
    - generic [ref=e91]:
      - term [ref=e92]: Connected Organization
      - definition [ref=e93]: DEMOCRATIC GRASSROOTS VICTORY FUND
  - generic [ref=e95]:
    - generic [ref=e96]:
      - heading "Candidates Funded" [level=2] [ref=e97]
      - group "View" [ref=e98]:
        - button "Sankey view" [pressed] [ref=e99] [cursor=pointer]
        - button "Table view" [ref=e103] [cursor=pointer]
    - paragraph [ref=e107]: "Candidates receiving contributions from this committee:"
    - img "Candidates Funded as a Sankey diagram. Switch to table view for the list." [ref=e108]
    - paragraph [ref=e111]: Showing top 20 of 439
  - generic [ref=e112]:
    - generic [ref=e113]:
      - heading "Independent Expenditures For" [level=2] [ref=e114]
      - group "View" [ref=e115]:
        - button "Sankey view" [pressed] [ref=e116] [cursor=pointer]
        - button "Table view" [ref=e120] [cursor=pointer]
    - paragraph [ref=e124]: "Candidates this committee spent for, independently of their campaigns (not contributions to the campaigns):"
    - img "Independent Expenditures For as a Sankey diagram. Switch to table view for the list." [ref=e125]
    - paragraph [ref=e128]: Showing all 6
  - generic [ref=e129]:
    - generic [ref=e130]:
      - heading "Independent Expenditures Against" [level=2] [ref=e131]
      - group "View" [ref=e132]:
        - button "Sankey view" [pressed] [ref=e133] [cursor=pointer]
        - button "Table view" [ref=e137] [cursor=pointer]
    - paragraph [ref=e141]: "Candidates this committee spent against, independently of their campaigns:"
    - img "Independent Expenditures Against as a Sankey diagram. Switch to table view for the list." [ref=e142]
    - paragraph [ref=e145]: Showing all 5
  - generic [ref=e146]:
    - generic [ref=e147]:
      - heading "Committees Funded" [level=2] [ref=e148]
      - group "View" [ref=e149]:
        - button "Sankey view" [pressed] [ref=e150] [cursor=pointer]
        - button "Table view" [ref=e154] [cursor=pointer]
    - paragraph [ref=e158]: "Committees receiving transfers from this committee:"
    - img "Committees Funded as a Sankey diagram. Switch to table view for the list." [ref=e159]
    - paragraph [ref=e162]: Showing top 20 of 1,646
  - generic [ref=e163]:
    - generic [ref=e164]:
      - heading "Funded By" [level=2] [ref=e165]
      - group "View" [ref=e166]:
        - button "Sankey view" [pressed] [ref=e167] [cursor=pointer]
        - button "Table view" [ref=e171] [cursor=pointer]
    - paragraph [ref=e175]: "Committees that have transferred funds to this committee:"
    - img "Funded By as a Sankey diagram. Switch to table view for the list." [ref=e176]
    - paragraph [ref=e179]: Showing top 20 of 2,261
  - contentinfo [ref=e180]:
    - paragraph [ref=e181]:
      - link "elect.info" [ref=e182] [cursor=pointer]:
        - /url: https://preview.elect.info/
      - text: "| Data from"
      - link "Federal Election Commission" [ref=e183] [cursor=pointer]:
        - /url: https://preview.elect.info/datasources/fec_bulk/
      - text: "| Generated 2026-10-16"
```

# 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 | test.describe('entity-graph pop-over', () => {
  63 |   for (const path of [DNC, HARRIS]) {
  64 |     test(`opens and draws on ${path}`, async ({ page }) => {
  65 |       const errors: string[] = [];
  66 |       page.on('pageerror', (e) => errors.push(String(e)));
  67 |       await page.goto(path);
  68 |       // The header button where the host has no accounts (CI pods); the identity bar's member control otherwise (sites#355).
  69 |       const button = page.locator('button.eg-open, #auth-slot [data-control="graph"]');
  70 |       await expect(button).toHaveCount(1, { timeout: 15000 });
> 71 |       await button.click();
     |                    ^ Error: locator.click: Test timeout of 30000ms exceeded.
  72 |       const dialog = page.locator('dialog.eg-dialog');
  73 |       await expect(dialog).toBeVisible();
  74 |       await expect(dialog.locator('.eg-canvas canvas').first()).toBeVisible({ timeout: 60000 });
  75 |       // centred in the viewport, horizontally and vertically
  76 |       const box = (await dialog.boundingBox())!;
  77 |       const vp = page.viewportSize()!;
  78 |       expect(Math.abs(box.x - (vp.width - box.x - box.width))).toBeLessThanOrEqual(2);
  79 |       expect(Math.abs(box.y - (vp.height - box.y - box.height))).toBeLessThanOrEqual(2);
  80 |       await page.keyboard.press('Escape');
  81 |       await expect(dialog).toBeHidden();
  82 |       expect(errors).toEqual([]);
  83 |     });
  84 |   }
  85 | });
  86 | 
```