# 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 >> graph pop-over on geography and leaf pages >> opens and draws on /states/032f8d61-f554-f435-2a6d-50b3c8cc01d7/
- Location: tests/ui/entity-graph.spec.ts:161:9

# Error details

```
Error: Test timeout of 30000ms exceeded
```

# Page snapshot

```yaml
- generic [active] [ref=e1]:
  - banner [ref=e2]:
    - heading "Alaska" [level=1] [ref=e3]
    - paragraph [ref=e4]: State Campaign Finance Profile
  - generic [ref=e5]:
    - generic [ref=e6]:
      - generic [ref=e7]:
        - generic "e2e-member@example.org" [ref=e8]
        - button "Registered" [ref=e10] [cursor=pointer]
      - generic [ref=e13]:
        - button "Request additional access" [ref=e14] [cursor=pointer]
        - link "Log out" [ref=e15] [cursor=pointer]:
          - /url: /outpost.goauthentik.io/sign_out
    - toolbar "Member tools" [ref=e16]:
      - 'button "All cycles (Needs Election cycles access: request it)" [disabled] [ref=e17] [cursor=pointer]':
        - generic [ref=e21]: All cycles
      - button "API keys" [ref=e24] [cursor=pointer]
      - 'button "Graph (Needs Graph access: request it)" [disabled] [ref=e29] [cursor=pointer]':
        - generic [ref=e38]: Graph
  - generic [ref=e41]:
    - heading "Overview" [level=2] [ref=e42]
    - generic [ref=e43]:
      - term [ref=e44]: State Code
      - definition [ref=e45]: AK
      - term [ref=e46]: Senate Seats
      - definition [ref=e47]: "2"
      - term [ref=e48]: House Seats
      - definition [ref=e49]: "1"
      - term [ref=e50]: Total Candidates
      - definition [ref=e51]: "299"
      - term [ref=e52]: Total Committees
      - definition [ref=e53]: "381"
      - term [ref=e54]: Active Cycles
      - definition [ref=e55]:
        - generic [ref=e56]:
          - link "2026" [ref=e57] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2026/
          - text: ","
          - link "2024" [ref=e58] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2024/
          - text: ","
          - link "2022" [ref=e59] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2022/
          - text: ","
          - link "2020" [ref=e60] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2020/
          - text: ","
          - link "2018" [ref=e61] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2018/
          - text: ","
          - link "2016" [ref=e62] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2016/
          - text: ","
          - link "2014" [ref=e63] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2014/
          - text: ","
          - link "2012" [ref=e64] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2012/
          - text: ","
          - link "2010" [ref=e65] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2010/
          - text: ","
          - link "2008" [ref=e66] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2008/
          - text: ","
          - link "2006" [ref=e67] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2006/
          - text: ","
          - link "2004" [ref=e68] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2004/
          - text: ","
          - link "2002" [ref=e69] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2002/
          - text: ","
          - link "2000" [ref=e70] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2000/
          - text: ","
          - link "1998" [ref=e71] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1998/
          - text: ","
          - link "1996" [ref=e72] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1996/
          - text: ","
          - link "1994" [ref=e73] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1994/
          - text: ","
          - link "1992" [ref=e74] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1992/
          - text: ","
          - link "1990" [ref=e75] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1990/
          - text: ","
          - link "1988" [ref=e76] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1988/
          - text: ","
          - link "1986" [ref=e77] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1986/
          - text: ","
          - link "1984" [ref=e78] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1984/
          - text: ","
          - link "1982" [ref=e79] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1982/
          - text: ","
          - link "1980" [ref=e80] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1980/
  - generic [ref=e81]:
    - heading "Candidates" [level=2] [ref=e82]
    - generic [ref=e84]:
      - generic [ref=e85]:
        - generic [ref=e86]:
          - combobox "Search entities" [ref=e87]
          - status [ref=e88]
        - button "Search" [ref=e89] [cursor=pointer]
      - generic [ref=e90]:
        - heading "Candidates" [level=3] [ref=e92]
        - generic [ref=e93]:
          - link "MURKOWSKI, LISA REP | $11,942,814" [ref=e94] [cursor=pointer]:
            - /url: /politicians/40c62658-928d-0c78-b95f-df5d24692b8e/
            - generic [ref=e95]: MURKOWSKI, LISA
            - generic [ref=e96]: REP | $11,942,814
          - link "YOUNG, DONALD E REP | $10,311,825" [ref=e97] [cursor=pointer]:
            - /url: /politicians/b9f432ca-0fa9-0fec-a6a9-173af899b483/
            - generic [ref=e98]: YOUNG, DONALD E
            - generic [ref=e99]: REP | $10,311,825
          - link "SULLIVAN, DAN REP | $8,491,967" [ref=e100] [cursor=pointer]:
            - /url: /politicians/4c649574-181f-4ce7-4b6c-d61dcf5f808b/
            - generic [ref=e101]: SULLIVAN, DAN
            - generic [ref=e102]: REP | $8,491,967
          - link "STEVENS, THEODORE F (TED) REP | $6,547,616" [ref=e103] [cursor=pointer]:
            - /url: /politicians/81caa461-623e-67e7-f285-2cef4bddfe06/
            - generic [ref=e104]: STEVENS, THEODORE F (TED)
            - generic [ref=e105]: REP | $6,547,616
          - link "BEGICH, MARK DEM | $4,343,871" [ref=e106] [cursor=pointer]:
            - /url: /politicians/0f5e6106-e966-0e6c-ac72-c2da306ccee2/
            - generic [ref=e107]: BEGICH, MARK
            - generic [ref=e108]: DEM | $4,343,871
          - link "MURKOWSKI, FRANK SENATOR REP | $3,405,278" [ref=e109] [cursor=pointer]:
            - /url: /politicians/5967f0d7-e919-92fa-4cbf-47940273c1cf/
            - generic [ref=e110]: MURKOWSKI, FRANK SENATOR
            - generic [ref=e111]: REP | $3,405,278
          - link "PELTOLA, MARY DEM | $2,521,323" [ref=e112] [cursor=pointer]:
            - /url: /politicians/df342087-9cf1-cd1e-3f36-5ee8b5893815/
            - generic [ref=e113]: PELTOLA, MARY
            - generic [ref=e114]: DEM | $2,521,323
          - link "PELTOLA, MARY DEM | $2,370,168" [ref=e115] [cursor=pointer]:
            - /url: /politicians/5220d211-639a-d377-f85d-7e183958410a/
            - generic [ref=e116]: PELTOLA, MARY
            - generic [ref=e117]: DEM | $2,370,168
          - link "BEGICH, NICHOLAS III REP | $1,748,931" [ref=e118] [cursor=pointer]:
            - /url: /politicians/6a7b5260-db82-c6d7-4d0a-34a71c9a7f40/
            - generic [ref=e119]: BEGICH, NICHOLAS III
            - generic [ref=e120]: REP | $1,748,931
          - link "KNOWLES, TONY DEM | $932,485" [ref=e121] [cursor=pointer]:
            - /url: /politicians/51a4b2fa-a508-4d6e-1ed9-04f2ca14221c/
            - generic [ref=e122]: KNOWLES, TONY
            - generic [ref=e123]: DEM | $932,485
          - link "GALVIN, ALYSE IND | $744,322" [ref=e124] [cursor=pointer]:
            - /url: /politicians/ee898688-6b91-a98c-ff58-b85aa4a67bf4/
            - generic [ref=e125]: GALVIN, ALYSE
            - generic [ref=e126]: IND | $744,322
          - link "MILLER, JOSEPH W LIB | $491,294" [ref=e127] [cursor=pointer]:
            - /url: /politicians/d8c9d0e1-4c3f-8db3-1922-f61a70b34b6a/
            - generic [ref=e128]: MILLER, JOSEPH W
            - generic [ref=e129]: LIB | $491,294
          - link "GROSS, AL DR. IND | $430,774" [ref=e130] [cursor=pointer]:
            - /url: /politicians/b188f9e1-83de-14b3-c499-af57fccd3ad9/
            - generic [ref=e131]: GROSS, AL DR.
            - generic [ref=e132]: IND | $430,774
          - link "MCADAMS, SCOTT DEM | $397,376" [ref=e133] [cursor=pointer]:
            - /url: /politicians/58e32dcd-294f-b8c0-ba7d-fca49b9d70a5/
            - generic [ref=e134]: MCADAMS, SCOTT
            - generic [ref=e135]: DEM | $397,376
          - link "SMITH, TONY DEM | $376,103" [ref=e136] [cursor=pointer]:
            - /url: /politicians/f259c6f8-2cd9-7bf0-a884-2af24759080b/
            - generic [ref=e137]: SMITH, TONY
            - generic [ref=e138]: DEM | $376,103
          - link "BERKOWITZ, ETHAN A DEM | $297,289" [ref=e139] [cursor=pointer]:
            - /url: /politicians/65531d38-e751-6884-e55c-aed3dd0af943/
            - generic [ref=e140]: BERKOWITZ, ETHAN A
            - generic [ref=e141]: DEM | $297,289
          - link "GRAVEL, MIKE DEM | $265,933" [ref=e142] [cursor=pointer]:
            - /url: /politicians/dc20e207-062e-09d2-bb76-a1674dcdc7b3/
            - generic [ref=e143]: GRAVEL, MIKE
            - generic [ref=e144]: DEM | $265,933
          - link "OLDS, GLENN A DEM | $264,541" [ref=e145] [cursor=pointer]:
            - /url: /politicians/8df89617-cba4-6691-0b74-5241acbe30e8/
            - generic [ref=e146]: OLDS, GLENN A
            - generic [ref=e147]: DEM | $264,541
          - link "LINDBECK, STEVE DEM | $258,685" [ref=e148] [cursor=pointer]:
            - /url: /politicians/8b8412d9-456f-6e9b-9e56-995f5b8c673a/
            - generic [ref=e149]: LINDBECK, STEVE
            - generic [ref=e150]: DEM | $258,685
          - link "DAHLSTROM, NANCY REP | $247,848" [ref=e151] [cursor=pointer]:
            - /url: /politicians/2868969a-8188-3e71-c441-d453943fff44/
            - generic [ref=e152]: DAHLSTROM, NANCY
            - generic [ref=e153]: REP | $247,848
          - link "DEVENS, JOHN S DEM | $173,151" [ref=e154] [cursor=pointer]:
            - /url: /politicians/2975649a-bf04-9cc3-0457-bd81501cab1a/
            - generic [ref=e155]: DEVENS, JOHN S
            - generic [ref=e156]: DEM | $173,151
          - link "DUNCAN, JIM (JAMES W DUNCAN) DEM | $141,597" [ref=e157] [cursor=pointer]:
            - /url: /politicians/4bbc0f9e-e9c3-54f3-550a-e2e7e0c7e6f6/
            - generic [ref=e158]: DUNCAN, JIM (JAMES W DUNCAN)
            - generic [ref=e159]: DEM | $141,597
          - link "GRUENSTEIN, PETER DEM | $127,081" [ref=e160] [cursor=pointer]:
            - /url: /politicians/5c5820cd-a0cd-7689-3d63-8bfe4ef58740/
            - generic [ref=e161]: GRUENSTEIN, PETER
            - generic [ref=e162]: DEM | $127,081
          - link "BEGICH, MARGARET (PEGGE) DEM | $122,569" [ref=e163] [cursor=pointer]:
            - /url: /politicians/96c6d941-1f42-8da7-288c-957a8eb3dafb/
            - generic [ref=e164]: BEGICH, MARGARET (PEGGE)
            - generic [ref=e165]: DEM | $122,569
          - link "SMITH, TONY DEM | $98,124" [ref=e166] [cursor=pointer]:
            - /url: /politicians/f3fe9b93-2f0f-e04e-39b2-52e8f9ee5a0e/
            - generic [ref=e167]: SMITH, TONY
            - generic [ref=e168]: DEM | $98,124
          - link "HILL, BILL IND | $90,460" [ref=e169] [cursor=pointer]:
            - /url: /politicians/653a2d2f-2d7c-aa0b-5a4e-9a8ef5a43de5/
            - generic [ref=e170]: HILL, BILL
            - generic [ref=e171]: IND | $90,460
          - link "PALIN, SARAH REP | $56,763" [ref=e172] [cursor=pointer]:
            - /url: /politicians/c430763c-a6c4-f585-2d8b-3162e8992a6a/
            - generic [ref=e173]: PALIN, SARAH
            - generic [ref=e174]: REP | $56,763
          - link "GRUENING, CLARK DEM | $45,012" [ref=e175] [cursor=pointer]:
            - /url: /politicians/7ae2afc9-47f7-4ee3-3c91-46de8cd04ee5/
            - generic [ref=e176]: GRUENING, CLARK
            - generic [ref=e177]: DEM | $45,012
          - link "DUNBAR, FORREST DEM | $34,217" [ref=e178] [cursor=pointer]:
            - /url: /politicians/56b37e1f-3a1b-60c9-5e04-24b40d54cb48/
            - generic [ref=e179]: DUNBAR, FORREST
            - generic [ref=e180]: DEM | $34,217
          - link "LINCOLN, GEORGIANNA DEM | $33,586" [ref=e181] [cursor=pointer]:
            - /url: /politicians/d4352f6a-b836-9af7-5853-0b4d0f8f96c7/
            - generic [ref=e182]: LINCOLN, GEORGIANNA
            - generic [ref=e183]: DEM | $33,586
          - link "SWEENEY, TARA M REP | $31,300" [ref=e184] [cursor=pointer]:
            - /url: /politicians/8b519f8c-c53c-399b-1b4d-cc3b3f038120/
            - generic [ref=e185]: SWEENEY, TARA M
            - generic [ref=e186]: REP | $31,300
          - link "KENNEDY, ARTHUR RALPH REP | $18,612" [ref=e187] [cursor=pointer]:
            - /url: /politicians/4b8880e0-1d40-2782-7914-e29ef56e9494/
            - generic [ref=e188]: KENNEDY, ARTHUR RALPH
            - generic [ref=e189]: REP | $18,612
          - link "GROSS, ALAN IND | $16,000" [ref=e190] [cursor=pointer]:
            - /url: /politicians/ef74021b-b108-2009-f431-fc373d24ac94/
            - generic [ref=e191]: GROSS, ALAN
            - generic [ref=e192]: IND | $16,000
          - link "TREADWELL, MEAD REP | $15,850" [ref=e193] [cursor=pointer]:
            - /url: /politicians/ade1903f-5ca0-6c7c-527b-2b9349718fe1/
            - generic [ref=e194]: TREADWELL, MEAD
            - generic [ref=e195]: REP | $15,850
          - link "BENSON, DIANE E MS DEM | $15,500" [ref=e196] [cursor=pointer]:
            - /url: /politicians/e6121050-d9ea-4858-a89f-27cf7d61b286/
            - generic [ref=e197]: BENSON, DIANE E MS
            - generic [ref=e198]: DEM | $15,500
          - link "CRAWFORD, HARRY T JR DEM | $13,560" [ref=e199] [cursor=pointer]:
            - /url: /politicians/2181e589-878b-10a1-9bc5-b017cc4fe980/
            - generic [ref=e200]: CRAWFORD, HARRY T JR
            - generic [ref=e201]: DEM | $13,560
          - link "COLLINS, VIRGINIA MARIE REP | $8,500" [ref=e202] [cursor=pointer]:
            - /url: /politicians/56caba81-a01a-37ca-6b66-053b49b9aff5/
            - generic [ref=e203]: COLLINS, VIRGINIA MARIE
            - generic [ref=e204]: REP | $8,500
          - link "HENSLEY, WILLIAM L DEM | $8,250" [ref=e205] [cursor=pointer]:
            - /url: /politicians/f46207f0-5198-0f22-1cba-09b5a8b85dfe/
            - generic [ref=e206]: HENSLEY, WILLIAM L
            - generic [ref=e207]: DEM | $8,250
          - link "HAVELOCK, JOHN ERIC DEM | $5,850" [ref=e208] [cursor=pointer]:
            - /url: /politicians/8302e084-0bf8-f13b-c56e-44a60e3eb4bc/
            - generic [ref=e209]: HAVELOCK, JOHN ERIC
            - generic [ref=e210]: DEM | $5,850
          - link "MILLER, MIKE REP | $5,500" [ref=e211] [cursor=pointer]:
            - /url: /politicians/14cae40c-79a5-895d-7e78-a6b418540e4c/
            - generic [ref=e212]: MILLER, MIKE
            - generic [ref=e213]: REP | $5,500
          - link "METCALFE, JAMES K JAKE DEM | $5,000" [ref=e214] [cursor=pointer]:
            - /url: /politicians/f85a4e73-a64b-1213-5cc1-4122cb1adf44/
            - generic [ref=e215]: METCALFE, JAMES K JAKE
            - generic [ref=e216]: DEM | $5,000
          - link "CHESBRO, PATRICIA DEM | $4,975" [ref=e217] [cursor=pointer]:
            - /url: /politicians/0eaa0537-bcef-47b8-e7df-5bf2808d53c0/
            - generic [ref=e218]: CHESBRO, PATRICIA
            - generic [ref=e219]: DEM | $4,975
          - link "CUDDY, DAVID W REP | $4,537" [ref=e220] [cursor=pointer]:
            - /url: /politicians/f769332d-a5ad-1953-1075-13fd3abcc2b7/
            - generic [ref=e221]: CUDDY, DAVID W
            - generic [ref=e222]: REP | $4,537
          - link "BEASLEY, MICHAEL J DEM | $4,500" [ref=e223] [cursor=pointer]:
            - /url: /politicians/5aefec02-fe89-86c1-1904-6eb4249521f2/
            - generic [ref=e224]: BEASLEY, MICHAEL J
            - generic [ref=e225]: DEM | $4,500
          - link "STOCK, MARGARET IND | $4,000" [ref=e226] [cursor=pointer]:
            - /url: /politicians/8233647f-159e-02b6-2e86-bf5449adead7/
            - generic [ref=e227]: STOCK, MARGARET
            - generic [ref=e228]: IND | $4,000
          - link "DAHL, THOMAS H DEM | $3,700" [ref=e229] [cursor=pointer]:
            - /url: /politicians/2f00615e-e47d-32e0-9e7e-b42308d1e2bf/
            - generic [ref=e230]: DAHL, THOMAS H
            - generic [ref=e231]: DEM | $3,700
          - link "SULLIVAN, DANIEL J REP | $3,500" [ref=e232] [cursor=pointer]:
            - /url: /politicians/6cc3191f-c8e9-a721-e88c-13ee92b7e10e/
            - generic [ref=e233]: SULLIVAN, DANIEL J
            - generic [ref=e234]: REP | $3,500
          - link "RODEY, PATRICK M DEM | $3,450" [ref=e235] [cursor=pointer]:
            - /url: /politicians/1e81a818-7585-e4aa-0055-7642a55bbb21/
            - generic [ref=e236]: RODEY, PATRICK M
            - generic [ref=e237]: DEM | $3,450
          - link "LEDOUX, GABRIELLE R REP | $3,427" [ref=e238] [cursor=pointer]:
            - /url: /politicians/f1e81780-a12d-2141-3b25-c8f6e07cd2b4/
            - generic [ref=e239]: LEDOUX, GABRIELLE R
            - generic [ref=e240]: REP | $3,427
          - link "BIRD, ROBERT MARIO IND | $3,000" [ref=e241] [cursor=pointer]:
            - /url: /politicians/5d304b09-9b38-5dcd-d81c-6b84433f0e69/
            - generic [ref=e242]: BIRD, ROBERT MARIO
            - generic [ref=e243]: IND | $3,000
        - generic [ref=e244]:
          - generic [ref=e245]: Showing 1-50
          - generic [ref=e246]:
            - button "First" [disabled] [ref=e247]
            - button "Prev" [disabled] [ref=e248]
            - button "1" [ref=e249] [cursor=pointer]
            - button "Next" [ref=e250] [cursor=pointer]
  - generic [ref=e251]:
    - heading "Committees" [level=2] [ref=e252]
    - paragraph [ref=e253]: "Top committees headquartered in Alaska:"
    - paragraph [ref=e254]: Showing top 20 of 381 committees
  - generic [ref=e255]:
    - heading "Congressional Districts" [level=2] [ref=e256]
    - paragraph [ref=e257]: Alaska has 1 seat in the U.S. House of Representatives.
  - contentinfo [ref=e258]:
    - paragraph [ref=e259]:
      - link "elect.info" [ref=e260] [cursor=pointer]:
        - /url: https://preview.elect.info/
      - text: "| Data from"
      - link "Federal Election Commission" [ref=e261] [cursor=pointer]:
        - /url: https://preview.elect.info/datasources/fec.gov/
      - text: "| Generated 2026-10-25"
```

# Test source

```ts
  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();
  146 |       await expect(dialog.locator('.eg-canvas canvas').first()).toBeVisible({ timeout: 60000 });
  147 |       // centred in the viewport, horizontally and vertically
  148 |       const box = (await dialog.boundingBox())!;
  149 |       const vp = page.viewportSize()!;
  150 |       expect(Math.abs(box.x - (vp.width - box.x - box.width))).toBeLessThanOrEqual(2);
  151 |       expect(Math.abs(box.y - (vp.height - box.y - box.height))).toBeLessThanOrEqual(2);
  152 |       await page.keyboard.press('Escape');
  153 |       await expect(dialog).toBeHidden();
  154 |       expect(errors).toEqual([]);
  155 |     });
  156 |   }
  157 | });
  158 | 
  159 | test.describe('graph pop-over on geography and leaf pages', () => {
  160 |   for (const path of [AK, LA_TRACT]) {
  161 |     test(`opens and draws on ${path}`, async ({ page, request }) => {
  162 |       const probe = await nodeGraph(request, 'County', LA_COUNTY);
  163 |       test.skip(probe.missing, 'the API has no nodeGraph yet');
  164 |       if (path === AK) test.skip(!(await (await request.get(AK)).text()).includes('data-graph-id'), 'the us-pages renderer predates GraphScript');
  165 |       const errors: string[] = [];
  166 |       page.on('pageerror', (e) => errors.push(String(e)));
  167 |       await asMember(page);
  168 |       await page.goto(path);
  169 |       const button = page.locator('#auth-slot [data-control="graph"]');
  170 |       await expect(button).toHaveCount(1, { timeout: 30000 });
  171 |       // the real control, not the placeholder hint (the leaf page registers it after rendering)
  172 |       await expect.poll(async () => {
  173 |         await button.click();
  174 |         return page.locator('dialog.eg-dialog').count();
> 175 |       }, { timeout: 30000 }).toBe(1);
      |                              ^ Error: Test timeout of 30000ms exceeded
  176 |       const dialog = page.locator('dialog.eg-dialog');
  177 |       await expect(dialog.locator('.eg-canvas canvas').first()).toBeVisible({ timeout: 60000 });
  178 |       await page.keyboard.press('Escape');
  179 |       await expect(dialog).toBeHidden();
  180 |       expect(errors).toEqual([]);
  181 |     });
  182 |   }
  183 | });
  184 | 
  185 | test.describe('expanding the graph one hop at a time', () => {
  186 |   test(`a county in ${AK}'s graph expands into the drawing`, async ({ page, request }) => {
  187 |     const probe = await nodeGraph(request, 'State', AK_ID);
  188 |     test.skip(probe.missing, 'the API has no nodeGraph yet');
  189 |     test.skip(!(await (await request.get(AK)).text()).includes('data-graph-id'), 'the us-pages renderer predates GraphScript');
  190 |     const county = probe.body?.data?.nodeGraph?.nodes?.find((n: { label: string }) => n.label === 'County');
  191 |     expect(county, 'Alaska has a county neighbour').toBeTruthy();
  192 | 
  193 |     const errors: string[] = [];
  194 |     page.on('pageerror', (e) => errors.push(String(e)));
  195 |     await asMember(page);
  196 |     await page.goto(AK);
  197 |     await page.locator('#auth-slot [data-control="graph"]').click({ timeout: 30000 });
  198 |     const dialog = page.locator('dialog.eg-dialog');
  199 |     await expect(dialog.locator('.eg-canvas canvas').first()).toBeVisible({ timeout: 60000 });
  200 |     type Hook = { electinfoGraph?: { nodeIds(): string[]; renderedPosition(id: string): { x: number; y: number } | null } };
  201 |     const count = () => page.evaluate(() => (document.querySelector('dialog.eg-dialog') as unknown as Hook).electinfoGraph!.nodeIds().length);
  202 |     const before = await count();
  203 |     const at = await page.evaluate((id) => (document.querySelector('dialog.eg-dialog') as unknown as Hook).electinfoGraph!.renderedPosition(id), county.id);
  204 |     expect(at).toBeTruthy();
  205 |     const box = (await dialog.locator('.eg-canvas').boundingBox())!;
  206 |     await page.mouse.click(box.x + at!.x, box.y + at!.y);
  207 |     const menu = dialog.locator('.eg-menu');
  208 |     await expect(menu).toContainText('County');
  209 |     await menu.locator('[data-eg-action="expand"]').click();
  210 |     await expect(menu.locator('[data-eg-status]')).toContainText(/added|No new connections/, { timeout: 60000 });
  211 |     expect(await count()).toBeGreaterThan(before);
  212 |     await expect(dialog.locator('.eg-foot')).toContainText(`Expanded: ${county.displayName}`);
  213 |     expect(errors).toEqual([]);
  214 |   });
  215 | });
  216 | 
  217 | test.describe('entity-graph without accounts', () => {
  218 |   test('a host without Authentik keeps the header Graph button', async ({ page }) => {
  219 |     await page.route('**/auth/whoami*', r => r.fulfill({ json: { enabled: 'false', email: '', elevated: false, entitlements: {} } }));
  220 |     await page.goto(DNC);
  221 |     await expect(page.locator('button.eg-open')).toHaveCount(1, { timeout: 15000 });
  222 |     await expect(page.locator('#auth-slot [data-control="graph"]')).toHaveCount(0);
  223 |   });
  224 | });
  225 | 
```