# 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) >> the money-flow Sankey and the Graph button work on a rendered candidate page
- Location: tests/ui/serving.spec.ts:164:7

# Error details

```
Error: expect(locator).toHaveCount(expected) failed

Locator:  locator('button.eg-open')
Expected: 1
Received: 0
Timeout:  15000ms

Call log:
  - Expect "toHaveCount" locator('button.eg-open') with timeout 15000ms
  - waiting for locator('button.eg-open')
    34 × locator resolved to 0 elements
       - unexpected value "0"

```

# Page snapshot

```yaml
- generic [active] [ref=e1]:
  - banner [ref=e2]:
    - heading "PELOSI, NANCY" [level=1] [ref=e3]
    - paragraph [ref=e4]: Democratic Party candidate for U.S. Representative - California District 11
    - paragraph [ref=e5]: "FEC ID: H8CA05035"
  - 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%2Fcandidates%2Fbfcafdd4-62da-f107-22fb-ff5413c33fa4%2F
        - link "Create an account" [ref=e12] [cursor=pointer]:
          - /url: /signin/register?rd=http%3A%2F%2Fpreview-us-elect-info.electinfo.svc.cluster.local%2Fcandidates%2Fbfcafdd4-62da-f107-22fb-ff5413c33fa4%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 "Profile" [level=2] [ref=e34]
    - generic [ref=e35]:
      - term [ref=e36]: Party
      - definition [ref=e37]:
        - link "Democratic Party" [ref=e38] [cursor=pointer]:
          - /url: https://preview.elect.info/parties/democratic/
      - term [ref=e39]: Office
      - definition [ref=e40]:
        - link "U.S. Representative - California District 11" [ref=e41] [cursor=pointer]:
          - /url: https://preview.elect.info/offices/house/ca-11/
      - term [ref=e42]: District
      - definition [ref=e43]:
        - link "California District 11" [ref=e44] [cursor=pointer]:
          - /url: https://preview.elect.info/districts/ca-11/
      - term [ref=e45]: Status
      - definition [ref=e46]: Statutory Candidate (Incumbent)
      - term [ref=e47]: Home Location
      - definition [ref=e48]: SAN FRANCISCO, California 94104
  - generic [ref=e49]:
    - heading "Campaign Timeline" [level=2] [ref=e50]
    - generic [ref=e51]:
      - term [ref=e52]: Election Years
      - definition [ref=e53]:
        - generic [ref=e54]:
          - link "2026" [ref=e55] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2026/
          - text: ","
          - link "2024" [ref=e56] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2024/
          - text: ","
          - link "2022" [ref=e57] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2022/
          - text: ","
          - link "2020" [ref=e58] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2020/
          - text: ","
          - link "2018" [ref=e59] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2018/
          - text: ","
          - link "2016" [ref=e60] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2016/
          - text: ","
          - link "2014" [ref=e61] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2014/
          - text: ","
          - link "2012" [ref=e62] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2012/
          - text: ","
          - link "2010" [ref=e63] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2010/
          - text: ","
          - link "2008" [ref=e64] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2008/
          - text: ","
          - link "2006" [ref=e65] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2006/
          - text: ","
          - link "2004" [ref=e66] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2004/
          - text: ","
          - link "2002" [ref=e67] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2002/
          - text: ","
          - link "2000" [ref=e68] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/2000/
          - text: ","
          - link "1998" [ref=e69] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1998/
          - text: ","
          - link "1996" [ref=e70] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1996/
          - text: ","
          - link "1994" [ref=e71] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1994/
          - text: ","
          - link "1992" [ref=e72] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1992/
          - text: ","
          - link "1990" [ref=e73] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1990/
          - text: ","
          - link "1988" [ref=e74] [cursor=pointer]:
            - /url: https://preview.elect.info/elections/1988/
      - term [ref=e75]: First Campaign
      - definition [ref=e76]: "1988"
      - term [ref=e77]: Most Recent
      - definition [ref=e78]: "2026"
  - generic [ref=e79]:
    - heading "Campaign Funding" [level=2] [ref=e80]
    - generic [ref=e81]:
      - term [ref=e82]: Total Received
      - definition [ref=e83]:
        - strong [ref=e84]: $15.5M
      - term [ref=e85]: Contributing Committees
      - definition [ref=e86]: "6949"
    - generic [ref=e87]:
      - heading "Top Contributors" [level=3] [ref=e88]
      - group "View" [ref=e89]:
        - button "Sankey view" [pressed] [ref=e90] [cursor=pointer]
        - button "Table view" [ref=e94] [cursor=pointer]
    - img "Top Contributors as a Sankey diagram. Switch to table view for the list." [ref=e98]
    - paragraph [ref=e101]: Showing top 10 of 1,060
  - generic [ref=e102]:
    - generic [ref=e103]:
      - heading "Independent Expenditures Supporting" [level=2] [ref=e104]
      - group "View" [ref=e105]:
        - button "Sankey view" [pressed] [ref=e106] [cursor=pointer]
        - button "Table view" [ref=e110] [cursor=pointer]
    - paragraph [ref=e114]: Spending by outside groups for this candidate, made independently of the campaign. Not a contribution to the campaign.
    - img "Independent Expenditures Supporting as a Sankey diagram. Switch to table view for the list." [ref=e115]
    - paragraph [ref=e118]: Showing top 10 of 51
  - generic [ref=e119]:
    - generic [ref=e120]:
      - heading "Independent Expenditures Opposing" [level=2] [ref=e121]
      - group "View" [ref=e122]:
        - button "Sankey view" [pressed] [ref=e123] [cursor=pointer]
        - button "Table view" [ref=e127] [cursor=pointer]
    - paragraph [ref=e131]: Spending by outside groups against this candidate, made independently of any campaign.
    - img "Independent Expenditures Opposing as a Sankey diagram. Switch to table view for the list." [ref=e132]
    - paragraph [ref=e135]: Showing top 10 of 25
  - contentinfo [ref=e136]:
    - paragraph [ref=e137]:
      - link "elect.info" [ref=e138] [cursor=pointer]:
        - /url: https://preview.elect.info/
      - text: "| Data from"
      - link "Federal Election Commission" [ref=e139] [cursor=pointer]:
        - /url: https://preview.elect.info/datasources/fec_bulk/
      - text: "| Generated 2026-10-16"
```

# Test source

```ts
  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}/`);
  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 |     await expect(page.locator('button.eg-open')).toHaveCount(1, { timeout: 15000 });
      |                                                  ^ Error: expect(locator).toHaveCount(expected) failed
  171 |     expect(errors).toEqual([]);
  172 |   });
  173 | });
  174 | 
```