# 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/leaf-details.spec.ts >> leaf collection client-side detail pages >> hub vendor resolves an alias without issuing a live connection query
- Location: tests/ui/leaf-details.spec.ts:206:7

# Error details

```
Error: expect(received).toHaveLength(expected)

Expected length: 3
Received length: 4
Received array:  ["{\"query\":\"\\n  query VendorProfile($id: String!) {\\n    vendors(where: { id: { eq: $id } }, limit: 1) {\\n      id name name_normalized city state total_received payment_count\\n      purpose_categories client_count first_payment last_payment cycles\\n    }\\n  }\\n\",\"variables\":{\"id\":\"consulting/hub-alias\"}}", "{\"query\":\"\\n  query VendorProfile($id: String!) {\\n    vendors(where: { id: { eq: $id } }, limit: 1) {\\n      id name name_normalized city state total_received payment_count\\n      purpose_categories client_count first_payment last_payment cycles\\n    }\\n  }\\n\",\"variables\":{\"id\":\"hub-vendor-id\"}}", "{\"query\":\"\\n  query VendorRankedClients($id: String!) {\\n    vendorClients(vendorId: $id) {\\n      ranked clientCount totalReceived otherCount otherAmount\\n      clients { committeeId name committeeType party affinityDem affinityRep amount count purposes rank }\\n    }\\n  }\\n\",\"variables\":{\"id\":\"hub-vendor-id\"}}", "{\"query\":\"\\n  query VendorPurposeFlow($id: String!, $cycle: Int) {\\n    vendorPurposeFlow(vendorId: $id, cycle: $cycle) {\\n      taxonomyVersion totalAmount\\n      groups { groupId group amount count clients topPurposes { purpose amount count } }\\n      links { committeeId name party affinityDem affinityRep groupId group amount count }\\n      otherLinks { groupId group amount count }\\n    }\\n  }\\n\",\"variables\":{\"id\":\"hub-vendor-id\",\"cycle\":null}}"]
```

# Page snapshot

```yaml
- generic [active] [ref=e1]:
  - banner [ref=e2]:
    - heading "HUB VENDOR" [level=1] [ref=e3]
    - paragraph [ref=e4]: Campaign Vendor
  - generic [ref=e5]:
    - heading "Overview" [level=2] [ref=e6]
    - generic [ref=e7]:
      - term [ref=e8]: Name
      - definition [ref=e9]: HUB VENDOR
      - term [ref=e10]: Normalized name
      - definition [ref=e11]: hub vendor
      - term [ref=e12]: Location
      - definition [ref=e13]: AUSTIN, TX
      - term [ref=e14]: Purpose categories
      - definition [ref=e15]: Not yet classified
      - term [ref=e16]: Cycles
      - definition [ref=e17]: 2020,2022,2024,2026
      - term [ref=e18]: First payment
      - definition [ref=e19]: Jan 1, 2020
      - term [ref=e20]: Last payment
      - definition [ref=e21]: Jan 1, 2026
  - generic [ref=e22]:
    - heading "Payment Activity" [level=2] [ref=e23]
    - generic [ref=e24]:
      - term [ref=e25]: Total received
      - definition [ref=e26]: $987,654,321.00
      - term [ref=e27]: Payments
      - definition [ref=e28]: 75,000
      - term [ref=e29]: Committee clients
      - definition [ref=e30]: 50,000
  - generic [ref=e31]:
    - heading "Spending by Purpose" [level=2] [ref=e32]
    - generic [ref=e33]:
      - generic [ref=e34]: Cycle
      - combobox "Purpose cycle" [ref=e35]:
        - option "All cycles" [selected]
        - option "2026"
        - option "2024"
        - option "2022"
        - option "2020"
    - generic [ref=e36]:
      - paragraph [ref=e37]: Purpose breakdown is not available for this selection.
      - group [ref=e38]:
        - generic "Raw purposes" [ref=e39] [cursor=pointer]
  - generic [ref=e40]:
    - heading "Top Committee Clients (up to 20)" [level=2] [ref=e41]
    - paragraph [ref=e42]: This profile has 50,000 related records. Totals are available above; the top-record list is deferred until the ranked-edge index is available.
  - contentinfo [ref=e43]:
    - paragraph [ref=e44]:
      - link "elect.info" [ref=e45] [cursor=pointer]:
        - /url: https://us.elect.info/
      - text: "|"
      - link "Browse Campaign Vendors" [ref=e46] [cursor=pointer]:
        - /url: /vendors/
```

# Test source

```ts
  159 |     await page.route('**/_resolve/**', async route => {
  160 |       resolveUrls.push(route.request().url());
  161 |       await route.fulfill({
  162 |         status: 200,
  163 |         contentType: 'application/json',
  164 |         body: JSON.stringify({ canonical_path: '/employers/hub-employer/' }),
  165 |       });
  166 |     });
  167 |     await page.route('**/api/graphql', async route => {
  168 |       const body = route.request().postData() || '';
  169 |       graphqlBodies.push(body);
  170 |       await route.fulfill({
  171 |         status: 200,
  172 |         contentType: 'application/json',
  173 |         body: JSON.stringify({
  174 |           data: {
  175 |             employers: [{
  176 |               id: 'hub-test',
  177 |               name: 'HUB EMPLOYER',
  178 |               name_normalized: 'hub employer',
  179 |               total_employee_contributions: 123456789,
  180 |               employee_donor_count: 50000,
  181 |               contribution_count: 75000,
  182 |               committee_count: 100,
  183 |               first_contribution: '2020-01-01',
  184 |               last_contribution: '2026-01-01',
  185 |               cycles: '2020,2022,2024,2026',
  186 |             }],
  187 |           },
  188 |         }),
  189 |       });
  190 |     });
  191 | 
  192 |     const response = await page.goto('/employers/hub%20test/');
  193 |     expect(response?.status()).toBe(200);
  194 |     await expect(page.locator('.leaf-detail-page h1')).toHaveText('HUB EMPLOYER', { timeout: DATA_LOAD_TIMEOUT });
  195 |     await expect(page.locator('.leaf-hub-deferred')).toContainText('50,000 related records');
  196 |     await expect(page.locator('link[rel="canonical"]')).toHaveAttribute(
  197 |       'href', 'https://us.elect.info/employers/hub-employer/'
  198 |     );
  199 |     expect(resolveUrls).toHaveLength(1);
  200 |     expect(new URL(resolveUrls[0]).pathname).toBe('/_resolve/employers/hub%20test');
  201 |     expect(graphqlBodies).toHaveLength(1);
  202 |     expect(graphqlBodies[0]).toContain('EmployerProfile');
  203 |     expect(graphqlBodies[0]).not.toContain('individualsEmployedByConnection');
  204 |   });
  205 | 
  206 |   test('hub vendor resolves an alias without issuing a live connection query', async ({ page }) => {
  207 |     const graphqlBodies: string[] = [];
  208 |     await page.route('**/_resolve/vendors/**', async route => {
  209 |       const path = new URL(route.request().url()).pathname;
  210 |       if (path.endsWith('/hub-alias')) {
  211 |         await route.fulfill({ status: 301, headers: { Location: '/vendors/hub-canonical/' } });
  212 |         return;
  213 |       }
  214 |       await route.fulfill({
  215 |         status: 200,
  216 |         contentType: 'application/json',
  217 |         body: JSON.stringify({
  218 |           entity_id: 'hub-vendor-id',
  219 |           canonical_path: '/vendors/hub-canonical/',
  220 |         }),
  221 |       });
  222 |     });
  223 |     await page.route('**/api/graphql', async route => {
  224 |       const body = route.request().postData() || '';
  225 |       graphqlBodies.push(body);
  226 |       const id = JSON.parse(body).variables?.id;
  227 |       await route.fulfill({
  228 |         status: 200,
  229 |         contentType: 'application/json',
  230 |         body: JSON.stringify({
  231 |           data: {
  232 |             vendors: id === 'hub-vendor-id' ? [{
  233 |               id: 'hub-vendor-id',
  234 |               name: 'HUB VENDOR',
  235 |               name_normalized: 'hub vendor',
  236 |               city: 'AUSTIN',
  237 |               state: 'TX',
  238 |               total_received: 987654321,
  239 |               payment_count: 75000,
  240 |               purpose_categories: 'MEDIA',
  241 |               client_count: 50000,
  242 |               first_payment: '2020-01-01',
  243 |               last_payment: '2026-01-01',
  244 |               cycles: '2020,2022,2024,2026',
  245 |             }] : [],
  246 |           },
  247 |         }),
  248 |       });
  249 |     });
  250 | 
  251 |     const response = await page.goto('/vendors/consulting/hub-alias/');
  252 |     expect(response?.status()).toBe(200);
  253 |     await expect(page.locator('.leaf-detail-page h1')).toHaveText('HUB VENDOR', { timeout: DATA_LOAD_TIMEOUT });
  254 |     await expect(page.locator('.leaf-hub-deferred')).toContainText('50,000 related records');
  255 |     await expect(page.locator('link[rel="canonical"]')).toHaveAttribute(
  256 |       'href', 'https://us.elect.info/vendors/hub-canonical/'
  257 |     );
  258 |     // Profile (alias, then canonical id) and the ranked list, which this graph does not have.
> 259 |     expect(graphqlBodies).toHaveLength(3);
      |                           ^ Error: expect(received).toHaveLength(expected)
  260 |     expect(graphqlBodies.filter(body => body.includes('VendorProfile'))).toHaveLength(2);
  261 |     expect(graphqlBodies.filter(body => body.includes('VendorRankedClients'))).toHaveLength(1);
  262 |     expect(graphqlBodies.some(body => body.includes('"id":"hub-vendor-id"'))).toBe(true);
  263 |     expect(graphqlBodies.every(body => !body.includes('committeesPaysConnection'))).toBe(true);
  264 |   });
  265 | 
  266 |   test('hub vendor lists its ranked top clients with a top-N-of-P footer and Sankey (sites#261)', async ({ page }) => {
  267 |     const graphqlBodies: string[] = [];
  268 |     await page.route('**/_resolve/**', route => route.fulfill({
  269 |       status: 200,
  270 |       contentType: 'application/json',
  271 |       body: JSON.stringify({ entity_id: 'hub-ranked', canonical_path: '/vendors/hub-ranked/' }),
  272 |     }));
  273 |     await page.route('**/api/graphql', async route => {
  274 |       const body = route.request().postData() || '';
  275 |       graphqlBodies.push(body);
  276 |       const data = body.includes('VendorRankedClients')
  277 |         ? {
  278 |             vendorClients: {
  279 |               ranked: true,
  280 |               clientCount: 61204,
  281 |               totalReceived: 987654321,
  282 |               otherCount: 61202,
  283 |               otherAmount: 900000000,
  284 |               clients: [
  285 |                 {
  286 |                   committeeId: 'c-dem', name: 'DEM VICTORY FUND', committeeType: 'N', party: 'DEM',
  287 |                   affinityDem: null, affinityRep: null, amount: 60000000, count: 1200, purposes: 'MEDIA BUY', rank: 1,
  288 |                 },
  289 |                 {
  290 |                   committeeId: 'c-rep', name: 'REP LEADERSHIP PAC', committeeType: 'Q', party: null,
  291 |                   affinityDem: 0.05, affinityRep: 0.95, amount: 27654321, count: 640, purposes: 'DIGITAL, CONSULTING', rank: 2,
  292 |                 },
  293 |               ],
  294 |             },
  295 |           }
  296 |         : {
  297 |             vendors: [{
  298 |               id: 'hub-ranked',
  299 |               name: 'RANKED HUB VENDOR',
  300 |               name_normalized: 'ranked hub vendor',
  301 |               city: 'AUSTIN',
  302 |               state: 'TX',
  303 |               total_received: 987654321,
  304 |               payment_count: 75000,
  305 |               purpose_categories: 'MEDIA',
  306 |               client_count: 61204,
  307 |               first_payment: '2020-01-01',
  308 |               last_payment: '2026-01-01',
  309 |               cycles: '2020,2022,2024,2026',
  310 |             }],
  311 |           };
  312 |       await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data }) });
  313 |     });
  314 | 
  315 |     const response = await page.goto('/vendors/hub-ranked/');
  316 |     expect(response?.status()).toBe(200);
  317 |     await expect(page.locator('.leaf-detail-page h1')).toHaveText('RANKED HUB VENDOR', { timeout: DATA_LOAD_TIMEOUT });
  318 |     await expect(page.locator('.leaf-hub-deferred')).toHaveCount(0);
  319 |     await expect(page.locator('#clients .leaf-list-footer')).toHaveText('Showing top 2 of 61,204 committees');
  320 |     await expect(page.locator('#clients .money-flow-chart canvas')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  321 | 
  322 |     await page.locator('#clients .mf-view-btn[data-view="table"]').click();
  323 |     const rows = page.locator('#clients .leaf-relationship-list tbody tr');
  324 |     await expect(rows).toHaveCount(2);
  325 |     await expect(rows.first()).toContainText('DEM VICTORY FUND');
  326 |     await expect(rows.first()).toContainText('MEDIA BUY');
  327 |     await expect(page.locator('#clients .leaf-list-footer')).toBeVisible();
  328 |     await page.locator('#clients .mf-view-btn[data-view="sankey"]').click();
  329 | 
  330 |     expect(graphqlBodies.every(body => !body.includes('committeesPaysConnection'))).toBe(true);
  331 |     expect(graphqlBodies.filter(body => body.includes('VendorRankedClients'))).toHaveLength(1);
  332 |   });
  333 | 
  334 |   test('individual Top Committees: one row per committee and a distinct-committee footer (sites#194)', async ({ page }) => {
  335 |     const graphqlBodies: string[] = [];
  336 |     await page.route('**/_resolve/**', route => route.fulfill({ status: 404, body: '' }));
  337 |     await page.route('**/api/graphql', async route => {
  338 |       const body = route.request().postData() || '';
  339 |       graphqlBodies.push(body);
  340 |       const data = body.includes('IndividualCommittees')
  341 |         ? {
  342 |             moneyCounterparties: {
  343 |               deferred: false,
  344 |               totalCount: 35,
  345 |               counterparties: [
  346 |                 // summed over the pair's per-cycle edges by the resolver (rundeck#758)
  347 |                 { id: 'c-1', name: 'FIRST VICTORY FUND', committeeType: 'N', amount: 12500, count: 7 },
  348 |                 { id: 'c-2', name: 'SECOND PAC', committeeType: 'Q', amount: 900, count: null },
  349 |               ],
  350 |             },
  351 |           }
  352 |         : {
  353 |             individuals: [{
  354 |               id: 'ind-194', name: 'DOE, JANE', city: 'AUSTIN', state: 'TX', zip_code: '78701',
  355 |               employer: 'SELF', occupation: 'WRITER', total_contributions: 13400, contribution_count: 40,
  356 |               committee_count: 35, first_contribution: '2020-01-01', last_contribution: '2024-10-01', cycles: '2020,2024',
  357 |               employedByEmployersConnection: { edges: [] },
  358 |               residesInPlacesConnection: { edges: [] },
  359 |               residesInPostalCodesConnection: { edges: [] },
```