# 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 >> individual Overview, Location and Districts link like the voter page, districts by ZIP overlap
- Location: tests/ui/leaf-details.spec.ts:350:7

# Error details

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

Locator: locator('.leaf-detail-page h1')
Expected: "MOLLICA, CAROL"
Timeout: 20000ms
Error: element(s) not found

Call log:
  - Expect "toHaveText" locator('.leaf-detail-page h1') with timeout 20000ms
  - waiting for locator('.leaf-detail-page h1')

```

```yaml
- region "Status messages"
- banner
- main "Authentication form":
  - text: elect.info
  - heading "Sign in to elect.info" [level=1]
  - paragraph: Login to continue to us.elect.info.
  - text: Email
  - textbox "Email"
  - text: Password
  - textbox "Password":
    - /placeholder: Please enter your password
  - button "Log in"
  - group "Login sources":
    - text: or Login sources
    - button "Continue with Google"
  - group "Additional actions":
    - text: Additional actions Need an account?
    - link "Sign up.":
      - /url: /if/flow/elect-info-enrollment/?client_id=KI5Be8scLVeZN7n5zY8p5s8VbpyradOSKQvo9T2c&redirect_uri=https%3A%2F%2Fus.elect.info%2Foutpost.goauthentik.io%2Fcallback%3FX-authentik-auth-callback%3Dtrue&response_type=code&scope=openid+entitlements+ak_proxy+email+profile&state=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnb2F1dGhlbnRpay5pby9vdXRwb3N0L0tJNUJlOHNjTFZlWk43bjV6WThwNXM4VmJweXJhZE9TS1F2bzlUMmMiLCJzaWQiOiI0WTNWN1JUS0tEN1pMNkhEMllYNlpJUjdMVlVMNzdZUTZXR1hFVzJaSVRCM0NLWDdENEFBIiwic3RhdGUiOiJYcEVWdGpBa1gyWExIOGtsVVM5R1RVNGlJUjVOeW9lWnliN3QxajFyQmJVIiwicmVkaXJlY3QiOiJodHRwczovL3VzLmVsZWN0LmluZm8vaW5kaXZpZHVhbHMvaW5kLXBhcml0eS8ifQ.t3ENJQdv4dbP8q5utiLYg7oUFo3SIwxvkIRgj4XfK1o&next=%2Fapplication%2Fo%2Fauthorize%2F%3Fclient_id%3DKI5Be8scLVeZN7n5zY8p5s8VbpyradOSKQvo9T2c%26redirect_uri%3Dhttps%253A%252F%252Fus.elect.info%252Foutpost.goauthentik.io%252Fcallback%253FX-authentik-auth-callback%253Dtrue%26response_type%3Dcode%26scope%3Dopenid%2Bentitlements%2Bak_proxy%2Bemail%2Bprofile%26state%3DeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnb2F1dGhlbnRpay5pby9vdXRwb3N0L0tJNUJlOHNjTFZlWk43bjV6WThwNXM4VmJweXJhZE9TS1F2bzlUMmMiLCJzaWQiOiI0WTNWN1JUS0tEN1pMNkhEMllYNlpJUjdMVlVMNzdZUTZXR1hFVzJaSVRCM0NLWDdENEFBIiwic3RhdGUiOiJYcEVWdGpBa1gyWExIOGtsVVM5R1RVNGlJUjVOeW9lWnliN3QxajFyQmJVIiwicmVkaXJlY3QiOiJodHRwczovL3VzLmVsZWN0LmluZm8vaW5kaXZpZHVhbHMvaW5kLXBhcml0eS8ifQ.t3ENJQdv4dbP8q5utiLYg7oUFo3SIwxvkIRgj4XfK1o
    - link "Forgot username or password?":
      - /url: /if/flow/elect-info-recovery/?client_id=KI5Be8scLVeZN7n5zY8p5s8VbpyradOSKQvo9T2c&redirect_uri=https%3A%2F%2Fus.elect.info%2Foutpost.goauthentik.io%2Fcallback%3FX-authentik-auth-callback%3Dtrue&response_type=code&scope=openid+entitlements+ak_proxy+email+profile&state=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnb2F1dGhlbnRpay5pby9vdXRwb3N0L0tJNUJlOHNjTFZlWk43bjV6WThwNXM4VmJweXJhZE9TS1F2bzlUMmMiLCJzaWQiOiI0WTNWN1JUS0tEN1pMNkhEMllYNlpJUjdMVlVMNzdZUTZXR1hFVzJaSVRCM0NLWDdENEFBIiwic3RhdGUiOiJYcEVWdGpBa1gyWExIOGtsVVM5R1RVNGlJUjVOeW9lWnliN3QxajFyQmJVIiwicmVkaXJlY3QiOiJodHRwczovL3VzLmVsZWN0LmluZm8vaW5kaXZpZHVhbHMvaW5kLXBhcml0eS8ifQ.t3ENJQdv4dbP8q5utiLYg7oUFo3SIwxvkIRgj4XfK1o&next=%2Fapplication%2Fo%2Fauthorize%2F%3Fclient_id%3DKI5Be8scLVeZN7n5zY8p5s8VbpyradOSKQvo9T2c%26redirect_uri%3Dhttps%253A%252F%252Fus.elect.info%252Foutpost.goauthentik.io%252Fcallback%253FX-authentik-auth-callback%253Dtrue%26response_type%3Dcode%26scope%3Dopenid%2Bentitlements%2Bak_proxy%2Bemail%2Bprofile%26state%3DeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnb2F1dGhlbnRpay5pby9vdXRwb3N0L0tJNUJlOHNjTFZlWk43bjV6WThwNXM4VmJweXJhZE9TS1F2bzlUMmMiLCJzaWQiOiI0WTNWN1JUS0tEN1pMNkhEMllYNlpJUjdMVlVMNzdZUTZXR1hFVzJaSVRCM0NLWDdENEFBIiwic3RhdGUiOiJYcEVWdGpBa1gyWExIOGtsVVM5R1RVNGlJUjVOeW9lWnliN3QxajFyQmJVIiwicmVkaXJlY3QiOiJodHRwczovL3VzLmVsZWN0LmluZm8vaW5kaXZpZHVhbHMvaW5kLXBhcml0eS8ifQ.t3ENJQdv4dbP8q5utiLYg7oUFo3SIwxvkIRgj4XfK1o
```

# Test source

```ts
  282 |   test('hub vendor lists its ranked top clients with a top-N-of-P footer and Sankey (sites#261)', async ({ page }) => {
  283 |     const graphqlBodies: string[] = [];
  284 |     await page.route('**/_resolve/**', route => route.fulfill({
  285 |       status: 200,
  286 |       contentType: 'application/json',
  287 |       body: JSON.stringify({ entity_id: 'hub-ranked', canonical_path: '/vendors/hub-ranked/' }),
  288 |     }));
  289 |     await page.route('**/api/graphql', async route => {
  290 |       const body = route.request().postData() || '';
  291 |       graphqlBodies.push(body);
  292 |       const data = body.includes('VendorRankedClients')
  293 |         ? {
  294 |             vendorClients: {
  295 |               ranked: true,
  296 |               clientCount: 61204,
  297 |               totalReceived: 987654321,
  298 |               otherCount: 61202,
  299 |               otherAmount: 900000000,
  300 |               clients: [
  301 |                 {
  302 |                   committeeId: 'c-dem', name: 'DEM VICTORY FUND', committeeType: 'N', party: 'DEM',
  303 |                   affinityDem: null, affinityRep: null, amount: 60000000, count: 1200, purposes: 'MEDIA BUY', rank: 1,
  304 |                 },
  305 |                 {
  306 |                   committeeId: 'c-rep', name: 'REP LEADERSHIP PAC', committeeType: 'Q', party: null,
  307 |                   affinityDem: 0.05, affinityRep: 0.95, amount: 27654321, count: 640, purposes: 'DIGITAL, CONSULTING', rank: 2,
  308 |                 },
  309 |               ],
  310 |             },
  311 |           }
  312 |         : {
  313 |             vendors: [{
  314 |               id: 'hub-ranked',
  315 |               name: 'RANKED HUB VENDOR',
  316 |               name_normalized: 'ranked hub vendor',
  317 |               city: 'AUSTIN',
  318 |               state: 'TX',
  319 |               total_received: 987654321,
  320 |               payment_count: 75000,
  321 |               purpose_categories: 'MEDIA',
  322 |               client_count: 61204,
  323 |               first_payment: '2020-01-01',
  324 |               last_payment: '2026-01-01',
  325 |               cycles: '2020,2022,2024,2026',
  326 |             }],
  327 |           };
  328 |       await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data }) });
  329 |     });
  330 | 
  331 |     const response = await page.goto('/vendors/hub-ranked/');
  332 |     expect(response?.status()).toBe(200);
  333 |     await expect(page.locator('.leaf-detail-page h1')).toHaveText('RANKED HUB VENDOR', { timeout: DATA_LOAD_TIMEOUT });
  334 |     await expect(page.locator('.leaf-hub-deferred')).toHaveCount(0);
  335 |     await expect(page.locator('#clients .leaf-list-footer')).toHaveText('Showing top 2 of 61,204 committees');
  336 |     await expect(page.locator('#clients .money-flow-chart canvas')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  337 | 
  338 |     await page.locator('#clients .mf-view-btn[data-view="table"]').click();
  339 |     const rows = page.locator('#clients .leaf-relationship-list tbody tr');
  340 |     await expect(rows).toHaveCount(2);
  341 |     await expect(rows.first()).toContainText('DEM VICTORY FUND');
  342 |     await expect(rows.first()).toContainText('MEDIA BUY');
  343 |     await expect(page.locator('#clients .leaf-list-footer')).toBeVisible();
  344 |     await page.locator('#clients .mf-view-btn[data-view="sankey"]').click();
  345 | 
  346 |     expect(graphqlBodies.every(body => !body.includes('committeesPaysConnection'))).toBe(true);
  347 |     expect(graphqlBodies.filter(body => body.includes('VendorRankedClients'))).toHaveLength(1);
  348 |   });
  349 | 
  350 |   test('individual Overview, Location and Districts link like the voter page, districts by ZIP overlap', async ({ page }) => {
  351 |     const errors: string[] = [];
  352 |     page.on('pageerror', error => errors.push(error.message));
  353 |     await page.route('**/_resolve/**', route => route.fulfill({ status: 404, body: '' }));
  354 |     await page.route('**/api/graphql', async route => {
  355 |       const body = route.request().postData() || '';
  356 |       const overlap = (node: Record<string, unknown>, area: number) => ({ node, properties: { overlap_land_area: area } });
  357 |       const data = body.includes('IndividualCommittees')
  358 |         ? { moneyCounterparties: { deferred: false, totalCount: 0, refundedInFullCount: 0, counterparties: [] } }
  359 |         : {
  360 |             individuals: [{
  361 |               id: 'ind-parity', name: 'MOLLICA, CAROL', city: 'WRENTHAM', state: 'MA', zip_code: '02093',
  362 |               employer: 'ACME CORP', occupation: 'TEACHER', total_contributions: 1200, contribution_count: 6,
  363 |               committee_count: 2, first_contribution: '2018-02-01', last_contribution: '2024-10-01', cycles: '2018,2024,2022',
  364 |               party_affinity_dem: 0.72, party_affinity_rep: 0.28,
  365 |               employedByEmployersConnection: { edges: [] },
  366 |               residesInPlacesConnection: { edges: [{ node: { id: 'place-wrentham', name: 'Wrentham CDP', name_normalized: 'WRENTHAM', state: 'MA' } }] },
  367 |               residesInPostalCodesConnection: { edges: [{ node: {
  368 |                 id: 'zip-02093', zip_code: '02093',
  369 |                 overlapsCountiesConnection: { edges: [overlap({ id: 'cty-norfolk', name: 'Norfolk County', state: 'MA' }, 56)] },
  370 |                 overlapsDistrictsConnection: { edges: [
  371 |                   overlap({ id: 'cd-8', name: 'Congressional District 8', state: 'MA', district_number: '08', district_slug: 'MA-08' }, 3),
  372 |                   overlap({ id: 'cd-4', name: 'Congressional District 4', state: 'MA', district_number: '04', district_slug: 'MA-04' }, 56),
  373 |                 ] },
  374 |               } }] },
  375 |             }],
  376 |           };
  377 |       await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data }) });
  378 |     });
  379 | 
  380 |     const response = await page.goto('/individuals/ind-parity/');
  381 |     expect(response?.status()).toBe(200);
> 382 |     await expect(page.locator('.leaf-detail-page h1')).toHaveText('MOLLICA, CAROL', { timeout: DATA_LOAD_TIMEOUT });
      |                                                        ^ Error: expect(locator).toHaveText(expected) failed
  383 |     const overview = page.locator('section#overview');
  384 |     await expect(overview.locator('a', { hasText: 'ACME CORP' })).toHaveAttribute('href', '/individuals/?f_employer=ACME+CORP');
  385 |     await expect(overview).toContainText('Party lean (giving)');
  386 |     await expect(overview).toContainText('Lean Democratic (72% D, 28% R)');
  387 |     await expect(overview.locator('a', { hasText: '2024' })).toHaveAttribute('href', '/individuals/?f_cycle=2024');
  388 |     await expect(overview).not.toContainText('Wrentham');
  389 | 
  390 |     const location = page.locator('section#location');
  391 |     const locationHrefs = await location.locator('a').evaluateAll(as => as.map(a => a.getAttribute('href')));
  392 |     expect(locationHrefs).toEqual([
  393 |       '/states/ma/', '/individuals/?f_state=MA',
  394 |       '/places/place-wrentham/', '/individuals/?f_state=MA&f_city=WRENTHAM',
  395 |       '/postalcodes/zip-02093/', '/individuals/?f_zip=02093',
  396 |       '/counties/cty-norfolk/',
  397 |     ]);
  398 |     await expect(page.locator('section#geography')).toHaveCount(0);
  399 | 
  400 |     const districts = page.locator('section#districts');
  401 |     await expect(districts).not.toContainText('donors');
  402 |     await expect(districts.locator('a', { hasText: 'MA-04' })).toHaveAttribute('href', '/districts/ma-04/');
  403 |     await expect(districts).toContainText('ZIP 02093 overlaps 2 districts');
  404 |     expect((await districts.textContent())!.indexOf('MA-04')).toBeLessThan((await districts.textContent())!.indexOf('MA-08'));
  405 |     await expect(districts).not.toContainText('Precinct');
  406 |     expect(errors).toEqual([]);
  407 |   });
  408 | 
  409 |   test('individual Top Committees: one row per committee and a distinct-committee footer (sites#194)', async ({ page }) => {
  410 |     const graphqlBodies: string[] = [];
  411 |     await page.route('**/_resolve/**', route => route.fulfill({ status: 404, body: '' }));
  412 |     await page.route('**/api/graphql', async route => {
  413 |       const body = route.request().postData() || '';
  414 |       graphqlBodies.push(body);
  415 |       const data = body.includes('IndividualCommittees')
  416 |         ? {
  417 |             moneyCounterparties: {
  418 |               deferred: false,
  419 |               totalCount: 35,
  420 |               counterparties: [
  421 |                 // summed over the pair's per-cycle edges by the resolver (rundeck#758)
  422 |                 { id: 'c-1', name: 'FIRST VICTORY FUND', committeeType: 'N', amount: 12500, count: 7 },
  423 |                 { id: 'c-2', name: 'SECOND PAC', committeeType: 'Q', amount: 900, count: null },
  424 |               ],
  425 |             },
  426 |           }
  427 |         : {
  428 |             individuals: [{
  429 |               id: 'ind-194', name: 'DOE, JANE', city: 'AUSTIN', state: 'TX', zip_code: '78701',
  430 |               employer: 'SELF', occupation: 'WRITER', total_contributions: 13400, contribution_count: 40,
  431 |               committee_count: 35, first_contribution: '2020-01-01', last_contribution: '2024-10-01', cycles: '2020,2024',
  432 |               employedByEmployersConnection: { edges: [] },
  433 |               residesInPlacesConnection: { edges: [] },
  434 |               residesInPostalCodesConnection: { edges: [] },
  435 |             }],
  436 |           };
  437 |       await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data }) });
  438 |     });
  439 | 
  440 |     const response = await page.goto('/individuals/ind-194/');
  441 |     expect(response?.status()).toBe(200);
  442 |     await expect(page.locator('.leaf-detail-page h1')).toHaveText('DOE, JANE', { timeout: DATA_LOAD_TIMEOUT });
  443 |     await page.locator('#committees .mf-view-btn[data-view="table"]').click();
  444 |     const rows = page.locator('#committees .leaf-relationship-list tbody tr');
  445 |     await expect(rows).toHaveCount(2);
  446 |     await expect(rows.first()).toContainText('FIRST VICTORY FUND');
  447 |     await expect(rows.first()).toContainText('$12,500');
  448 |     await expect(rows.nth(1).locator('td').nth(3)).toHaveText('—');
  449 |     await expect(page.locator('#committees .leaf-list-footer')).toHaveText('Showing top 2 of 35 committees');
  450 |     const committees = graphqlBodies.find(body => body.includes('IndividualCommittees')) || '';
  451 |     expect(committees).toContain('relationship: CONTRIBUTED_TO, direction: OUT, first: 20');
  452 |     expect(graphqlBodies.every(body => !body.includes('contributedToCommitteesConnection'))).toBe(true);
  453 |   });
  454 | 
  455 |   test('individual Top Committees: Sankey by category, decoded types, refund columns only when refunded (sites#413)', async ({ page }) => {
  456 |     const errors: string[] = [];
  457 |     page.on('pageerror', error => errors.push(error.message));
  458 |     await page.route('**/_resolve/**', route => route.fulfill({ status: 404, body: '' }));
  459 |     await page.route('**/api/graphql', async route => {
  460 |       const body = route.request().postData() || '';
  461 |       const row = (id: string, name: string, committeeType: string, party: string | null, amount: number) =>
  462 |         ({ id, name, committeeType, party, affinityDem: null, affinityRep: null, amount, gross: amount, refunded: 0, count: 1 });
  463 |       const data = body.includes('IndividualCommittees')
  464 |         ? {
  465 |             moneyCounterparties: {
  466 |               deferred: false,
  467 |               totalCount: 4,
  468 |               refundedInFullCount: 0,
  469 |               counterparties: [
  470 |                 row('c-sp', 'BIG SUPER PAC', 'O', null, 175000000),
  471 |                 row('c-pty', 'PARTY COMMITTEE', 'Y', 'DEM', 250000),
  472 |                 row('c-s', 'SENATOR FOR SENATE', 'S', 'DEM', 3300),
  473 |                 row('c-h', 'REP FOR CONGRESS', 'H', 'REP', 1000),
  474 |               ],
  475 |             },
  476 |           }
  477 |         : {
  478 |             individuals: [{
  479 |               id: 'ind-sankey', name: 'DONOR, BIG', city: 'NEW YORK', state: 'NY', zip_code: '10001',
  480 |               employer: 'SELF', occupation: 'INVESTOR', total_contributions: 175254300, contribution_count: 4,
  481 |               committee_count: 4, first_contribution: '2020-01-01', last_contribution: '2024-10-01', cycles: '2020,2024',
  482 |               employedByEmployersConnection: { edges: [] },
```