# 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 lists its ranked top clients with a top-N-of-P footer and Sankey (sites#261)
- Location: tests/ui/leaf-details.spec.ts:285:7

# Error details

```
Error: page.goto: net::ERR_CONNECTION_REFUSED at http://10.1.105.22/vendors/hub-ranked/
Call log:
  - navigating to "http://10.1.105.22/vendors/hub-ranked/", waiting until "load"

```

# Test source

```ts
  234 |       const id = JSON.parse(body).variables?.id;
  235 |       if (body.includes('VendorPurposeFlow')) {
  236 |         await route.fulfill({ json: { data: { vendorPurposeFlow: {
  237 |           taxonomyVersion: null, totalAmount: 0, groups: [], links: [], otherLinks: [],
  238 |         } } } });
  239 |         return;
  240 |       }
  241 |       await route.fulfill({
  242 |         status: 200,
  243 |         contentType: 'application/json',
  244 |         body: JSON.stringify({
  245 |           data: {
  246 |             vendors: id === 'hub-vendor-id' ? [{
  247 |               id: 'hub-vendor-id',
  248 |               name: 'HUB VENDOR',
  249 |               name_normalized: 'hub vendor',
  250 |               city: 'AUSTIN',
  251 |               state: 'TX',
  252 |               total_received: 987654321,
  253 |               payment_count: 75000,
  254 |               purpose_categories: 'MEDIA',
  255 |               client_count: 50000,
  256 |               first_payment: '2020-01-01',
  257 |               last_payment: '2026-01-01',
  258 |               cycles: '2020,2022,2024,2026',
  259 |             }] : [],
  260 |           },
  261 |         }),
  262 |       });
  263 |     });
  264 | 
  265 |     const response = await page.goto('/vendors/consulting/hub-alias/');
  266 |     expect(response?.status()).toBe(200);
  267 |     await expect(page.locator('.leaf-detail-page h1')).toHaveText('HUB VENDOR', { timeout: DATA_LOAD_TIMEOUT });
  268 |     await expect(page.locator('.leaf-hub-deferred')).toContainText('50,000 related records');
  269 |     await expect(page.locator('link[rel="canonical"]')).toHaveAttribute(
  270 |       'href', 'https://us.elect.info/vendors/hub-canonical/'
  271 |     );
  272 |     // Alias/canonical profiles and bounded ranked/purpose reads are allowed.
  273 |     // The hub guard must still suppress both live-client query implementations.
  274 |     expect(graphqlBodies.filter(body => body.includes('VendorProfile'))).toHaveLength(2);
  275 |     expect(graphqlBodies.filter(body => body.includes('VendorRankedClients'))).toHaveLength(1);
  276 |     const purposeReads = graphqlBodies.filter(body => body.includes('VendorPurposeFlow'));
  277 |     expect(purposeReads).toHaveLength(1);
  278 |     expect(JSON.parse(purposeReads[0]).variables).toEqual({ id: 'hub-vendor-id', cycle: null });
  279 |     expect(graphqlBodies.some(body => body.includes('"id":"hub-vendor-id"'))).toBe(true);
  280 |     expect(graphqlBodies.every(body => !body.includes('committeesPaysConnection'))).toBe(true);
  281 |     expect(graphqlBodies.every(body => !body.includes('moneyCounterparties'))).toBe(true);
  282 |     await expect(page.locator('#purposes .purpose-raw')).toBeVisible();
  283 |   });
  284 | 
  285 |   test('hub vendor lists its ranked top clients with a top-N-of-P footer and Sankey (sites#261)', async ({ page }) => {
  286 |     const graphqlBodies: string[] = [];
  287 |     await page.route('**/_resolve/**', route => route.fulfill({
  288 |       status: 200,
  289 |       contentType: 'application/json',
  290 |       body: JSON.stringify({ entity_id: 'hub-ranked', canonical_path: '/vendors/hub-ranked/' }),
  291 |     }));
  292 |     await page.route('**/api/graphql', async route => {
  293 |       const body = route.request().postData() || '';
  294 |       graphqlBodies.push(body);
  295 |       const data = body.includes('VendorRankedClients')
  296 |         ? {
  297 |             vendorClients: {
  298 |               ranked: true,
  299 |               clientCount: 61204,
  300 |               totalReceived: 987654321,
  301 |               otherCount: 61202,
  302 |               otherAmount: 900000000,
  303 |               clients: [
  304 |                 {
  305 |                   committeeId: 'c-dem', name: 'DEM VICTORY FUND', committeeType: 'N', party: 'DEM',
  306 |                   affinityDem: null, affinityRep: null, amount: 60000000, count: 1200, purposes: 'MEDIA BUY', rank: 1,
  307 |                 },
  308 |                 {
  309 |                   committeeId: 'c-rep', name: 'REP LEADERSHIP PAC', committeeType: 'Q', party: null,
  310 |                   affinityDem: 0.05, affinityRep: 0.95, amount: 27654321, count: 640, purposes: 'DIGITAL, CONSULTING', rank: 2,
  311 |                 },
  312 |               ],
  313 |             },
  314 |           }
  315 |         : {
  316 |             vendors: [{
  317 |               id: 'hub-ranked',
  318 |               name: 'RANKED HUB VENDOR',
  319 |               name_normalized: 'ranked hub vendor',
  320 |               city: 'AUSTIN',
  321 |               state: 'TX',
  322 |               total_received: 987654321,
  323 |               payment_count: 75000,
  324 |               purpose_categories: 'MEDIA',
  325 |               client_count: 61204,
  326 |               first_payment: '2020-01-01',
  327 |               last_payment: '2026-01-01',
  328 |               cycles: '2020,2022,2024,2026',
  329 |             }],
  330 |           };
  331 |       await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data }) });
  332 |     });
  333 | 
> 334 |     const response = await page.goto('/vendors/hub-ranked/');
      |                                 ^ Error: page.goto: net::ERR_CONNECTION_REFUSED at http://10.1.105.22/vendors/hub-ranked/
  335 |     expect(response?.status()).toBe(200);
  336 |     await expect(page.locator('.leaf-detail-page h1')).toHaveText('RANKED HUB VENDOR', { timeout: DATA_LOAD_TIMEOUT });
  337 |     await expect(page.locator('.leaf-hub-deferred')).toHaveCount(0);
  338 |     await expect(page.locator('#clients .leaf-list-footer')).toHaveText('Showing top 2 of 61,204 committees');
  339 |     await expect(page.locator('#clients .money-flow-chart canvas')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  340 | 
  341 |     await page.locator('#clients .mf-view-btn[data-view="table"]').click();
  342 |     const rows = page.locator('#clients .leaf-relationship-list tbody tr');
  343 |     await expect(rows).toHaveCount(2);
  344 |     await expect(rows.first()).toContainText('DEM VICTORY FUND');
  345 |     await expect(rows.first()).toContainText('MEDIA BUY');
  346 |     await expect(page.locator('#clients .leaf-list-footer')).toBeVisible();
  347 |     await page.locator('#clients .mf-view-btn[data-view="sankey"]').click();
  348 | 
  349 |     expect(graphqlBodies.every(body => !body.includes('committeesPaysConnection'))).toBe(true);
  350 |     expect(graphqlBodies.filter(body => body.includes('VendorRankedClients'))).toHaveLength(1);
  351 |   });
  352 | 
  353 |   test('individual Overview, Location and Districts link like the voter page, districts by ZIP overlap', async ({ page, request }) => {
  354 |     await skipIfPremiumGated(request, '/individuals/');
  355 |     const errors: string[] = [];
  356 |     page.on('pageerror', error => errors.push(error.message));
  357 |     await page.route('**/_resolve/**', route => route.fulfill({ status: 404, body: '' }));
  358 |     await page.route('**/api/graphql', async route => {
  359 |       const body = route.request().postData() || '';
  360 |       const overlap = (node: Record<string, unknown>, area: number) => ({ node, properties: { overlap_land_area: area } });
  361 |       const data = body.includes('IndividualCommittees')
  362 |         ? { moneyCounterparties: { deferred: false, totalCount: 0, refundedInFullCount: 0, counterparties: [] } }
  363 |         : {
  364 |             individuals: [{
  365 |               id: 'ind-parity', name: 'MOLLICA, CAROL', city: 'WRENTHAM', state: 'MA', zip_code: '02093',
  366 |               employer: 'ACME CORP', occupation: 'TEACHER', total_contributions: 1200, contribution_count: 6,
  367 |               committee_count: 2, first_contribution: '2018-02-01', last_contribution: '2024-10-01', cycles: '2018,2024,2022',
  368 |               party_affinity_dem: 0.72, party_affinity_rep: 0.28,
  369 |               employedByEmployersConnection: { edges: [] },
  370 |               residesInPlacesConnection: { edges: [{ node: { id: 'place-wrentham', name: 'Wrentham CDP', name_normalized: 'WRENTHAM', state: 'MA' } }] },
  371 |               residesInPostalCodesConnection: { edges: [{ node: {
  372 |                 id: 'zip-02093', zip_code: '02093',
  373 |                 overlapsCountiesConnection: { edges: [overlap({ id: 'cty-norfolk', name: 'Norfolk County', state: 'MA' }, 56)] },
  374 |                 overlapsDistrictsConnection: { edges: [
  375 |                   overlap({ id: 'cd-8', name: 'Congressional District 8', state: 'MA', district_number: '08', district_slug: 'MA-08' }, 3),
  376 |                   overlap({ id: 'cd-4', name: 'Congressional District 4', state: 'MA', district_number: '04', district_slug: 'MA-04' }, 56),
  377 |                 ] },
  378 |               } }] },
  379 |             }],
  380 |           };
  381 |       await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data }) });
  382 |     });
  383 | 
  384 |     const response = await page.goto('/individuals/ind-parity/');
  385 |     expect(response?.status()).toBe(200);
  386 |     await expect(page.locator('.leaf-detail-page h1')).toHaveText('MOLLICA, CAROL', { timeout: DATA_LOAD_TIMEOUT });
  387 |     const overview = page.locator('section#overview');
  388 |     await expect(overview.locator('a', { hasText: 'ACME CORP' })).toHaveAttribute('href', '/individuals/?f_employer=ACME+CORP');
  389 |     await expect(overview).toContainText('Party lean (giving)');
  390 |     await expect(overview).toContainText('Lean Democratic (72% D, 28% R)');
  391 |     await expect(overview.locator('a', { hasText: '2024' })).toHaveAttribute('href', '/individuals/?f_cycle=2024');
  392 |     await expect(overview).not.toContainText('Wrentham');
  393 | 
  394 |     const location = page.locator('section#location');
  395 |     const locationHrefs = await location.locator('a').evaluateAll(as => as.map(a => a.getAttribute('href')));
  396 |     expect(locationHrefs).toEqual([
  397 |       '/states/ma/', '/individuals/?f_state=MA',
  398 |       '/places/place-wrentham/', '/individuals/?f_state=MA&f_city=WRENTHAM',
  399 |       '/postalcodes/zip-02093/', '/individuals/?f_zip=02093',
  400 |       '/counties/cty-norfolk/',
  401 |     ]);
  402 |     await expect(page.locator('section#geography')).toHaveCount(0);
  403 | 
  404 |     const districts = page.locator('section#districts');
  405 |     await expect(districts).not.toContainText('donors');
  406 |     await expect(districts.locator('a', { hasText: 'MA-04' })).toHaveAttribute('href', '/districts/ma-04/');
  407 |     await expect(districts).toContainText('ZIP 02093 overlaps 2 districts');
  408 |     expect((await districts.textContent())!.indexOf('MA-04')).toBeLessThan((await districts.textContent())!.indexOf('MA-08'));
  409 |     await expect(districts).not.toContainText('Precinct');
  410 |     expect(errors).toEqual([]);
  411 |   });
  412 | 
  413 |   test('individual Top Committees: one row per committee and a distinct-committee footer (sites#194)', async ({ page, request }) => {
  414 |     await skipIfPremiumGated(request, '/individuals/');
  415 |     const graphqlBodies: string[] = [];
  416 |     await page.route('**/_resolve/**', route => route.fulfill({ status: 404, body: '' }));
  417 |     await page.route('**/api/graphql', async route => {
  418 |       const body = route.request().postData() || '';
  419 |       graphqlBodies.push(body);
  420 |       const data = body.includes('IndividualCommittees')
  421 |         ? {
  422 |             moneyCounterparties: {
  423 |               deferred: false,
  424 |               totalCount: 35,
  425 |               counterparties: [
  426 |                 // summed over the pair's per-cycle edges by the resolver (rundeck#758)
  427 |                 { id: 'c-1', name: 'FIRST VICTORY FUND', committeeType: 'N', amount: 12500, count: 7 },
  428 |                 { id: 'c-2', name: 'SECOND PAC', committeeType: 'Q', amount: 900, count: null },
  429 |               ],
  430 |             },
  431 |           }
  432 |         : {
  433 |             individuals: [{
  434 |               id: 'ind-194', name: 'DOE, JANE', city: 'AUSTIN', state: 'TX', zip_code: '78701',
```