# 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 >> employer detail renders totals and a bounded donor list below threshold
- Location: tests/ui/leaf-details.spec.ts:89:7

# Error details

```
Test timeout of 30000ms exceeded.
```

```
Error: page.goto: net::ERR_ABORTED; maybe frame was detached?
Call log:
  - navigating to "http://10.1.105.22/employers/5f9a1727-2225-3699-685b-8fb15c17c8f3/", waiting until "load"

```

# Test source

```ts
  1   | import { test, expect, Page } from '@playwright/test';
  2   | import { premiumVisitor, skipIfPremiumGated } from './helpers';
  3   | 
  4   | const DATA_LOAD_TIMEOUT = 20_000;
  5   | 
  6   | const samples = {
  7   |   individual: {
  8   |     path: '/individuals/93964649-04c3-db10-7f63-261c8c488cce/',
  9   |     heading: /BLOOMBERG, MICHAEL/i,
  10  |     operation: 'IndividualDetail',
  11  |     // moneyCounterparties (sites#194), or IndividualCommitteesLegacy on an API without it
  12  |     relationship: 'IndividualCommittees',
  13  |   },
  14  |   employer: {
  15  |     path: '/employers/5f9a1727-2225-3699-685b-8fb15c17c8f3/',
  16  |     heading: /SPACE EXPLORATION TECHNOLOGIES/i,
  17  |     operation: 'EmployerDonors',
  18  |     relationship: 'individualsEmployedByConnection',
  19  |   },
  20  |   vendor: {
  21  |     path: '/vendors/a2e9cfea-dee0-2824-46f2-843916cd2199/',
  22  |     heading: /TARGETED VICTORY/i,
  23  |     operation: 'VendorRankedClients',
  24  |     relationship: 'vendorClients',
  25  |   },
  26  |   block: {
  27  |     path: '/blocks/a2e648d3-3cd7-613b-1bfe-85ea19fad69a/',
  28  |     heading: /Block 1020/i,
  29  |     operation: 'BlockDetail',
  30  |     relationship: 'inCountyCountiesConnection',
  31  |   },
  32  |   precinct: {
  33  |     path: '/precincts/2ff858ff-68ec-a17d-a564-4e71054573c6/',
  34  |     heading: /Providence/i,
  35  |     operation: 'PrecinctDetail',
  36  |     relationship: 'inCountyCountiesConnection',
  37  |   },
  38  | };
  39  | 
  40  | async function openLeafDetail(
  41  |   page: Page,
  42  |   sample: { path: string; heading: RegExp; operation: string; relationship: string },
  43  |   bounded = true,
  44  | ): Promise<string[]> {
  45  |   const graphqlBodies: string[] = [];
  46  |   page.on('request', request => {
  47  |     if (request.method() === 'POST' && request.url().includes('/api/graphql')) {
  48  |       graphqlBodies.push(request.postData() || '');
  49  |     }
  50  |   });
  51  | 
> 52  |   const response = await page.goto(sample.path);
      |                               ^ Error: page.goto: net::ERR_ABORTED; maybe frame was detached?
  53  |   expect(response?.status(), `${sample.path} should serve the baked shell`).toBe(200);
  54  |   await expect(page.locator('.leaf-detail-page h1')).toHaveText(sample.heading, { timeout: DATA_LOAD_TIMEOUT });
  55  | 
  56  |   await expect(page).toHaveTitle(/elect\.info$/i);
  57  |   await expect(page.locator('meta[name="robots"]')).toHaveAttribute('content', 'noindex, follow');
  58  |   await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href', /^https:\/\/us\.elect\.info\//);
  59  |   await expect(page.locator('section#overview')).toBeVisible();
  60  | 
  61  |   expect(graphqlBodies.some(body => body.includes(sample.operation)), `${sample.operation} request missing`).toBe(true);
  62  |   if (!bounded) return graphqlBodies;
  63  |   const boundedBody = graphqlBodies.find(body => body.includes(sample.relationship));
  64  |   expect(boundedBody, `${sample.relationship} request missing`).toBeTruthy();
  65  |   expect(boundedBody).toContain('first: 20');
  66  |   return graphqlBodies;
  67  | }
  68  | 
  69  | test.describe('leaf collection client-side detail pages', () => {
  70  |   test('individual detail renders a bounded contribution profile', async ({ page, request, baseURL }) => {
  71  |     await skipIfPremiumGated(request, '/individuals/');
  72  |     await premiumVisitor(page.context(), baseURL); // donor records are premium (sites#277)
  73  |     const bodies = await openLeafDetail(page, samples.individual);
  74  |     const detail = bodies.find(body => body.includes('IndividualDetail')) || '';
  75  |     expect(detail).not.toContain('contributedToCommitteesConnection');
  76  |     // employers, places, postal codes, and each postal code's county and district overlaps
  77  |     expect(detail.match(/first:\s*20/g) || []).toHaveLength(5);
  78  |     await expect(page.locator('section#location a', { hasText: 'donors' }).first()).toBeVisible();
  79  |     await expect(page.locator('section#geography')).toHaveCount(0);
  80  |     expect(bodies.some(body => body.includes('query IndividualCommittees(') && body.includes('moneyCounterparties'))).toBe(true);
  81  |     const legacy = bodies.find(body => body.includes('IndividualCommitteesLegacy'));
  82  |     if (legacy) expect(legacy).toContain('sort: [{ edge: { total_amount: DESC } }]');
  83  |     // Two or more committees mount the Sankey (sites#413), hiding the table; one stays a table.
  84  |     await expect(page.locator('#committees .leaf-relationship-list')).toHaveCount(1);
  85  |     await expect(page.locator('#committees .money-flow-chart canvas, #committees .leaf-relationship-list').first()).toBeVisible();
  86  |     await expect(page.locator('#committees .leaf-list-footer')).toContainText(/Showing (top \d+ of [\d,]+|all [\d,]+) committees/);
  87  |   });
  88  | 
  89  |   test('employer detail renders totals and a bounded donor list below threshold', async ({ page, baseURL }) => {
  90  |     await premiumVisitor(page.context(), baseURL); // donor records are premium (sites#277)
  91  |     const bodies = await openLeafDetail(page, samples.employer);
  92  |     const donors = bodies.find(body => body.includes('EmployerDonors')) || '';
  93  |     expect(donors.match(/first:\s*20/g) || []).toHaveLength(1);
  94  |     await expect(page.locator('#activity')).toContainText('Employee donors');
  95  |     await expect(page.locator('#donors .leaf-relationship-list')).toBeVisible();
  96  |   });
  97  | 
  98  |   test('vendor detail renders totals and committee clients, ranked or edge-sorted', async ({ page }) => {
  99  |     const bodies = await openLeafDetail(page, samples.vendor, false);
  100 |     await expect(page.locator('#clients .leaf-list-footer')).toContainText(/Showing (top \d+ of [\d,]+|all [\d,]+) committees/, {
  101 |       timeout: DATA_LOAD_TIMEOUT,
  102 |     });
  103 |     await expect(page.locator('#clients .leaf-relationship-list')).toHaveCount(1);
  104 |     // The ranked list (sites#261) replaces the live list once the graph has PAYS ranks; until then
  105 |     // moneyCounterparties (sites#194), or on an API without it the bounded edge-sorted connection.
  106 |     const live = bodies.find(body => body.includes('query VendorClients('));
  107 |     if (live) {
  108 |       expect(live).toContain('sort: [{ edge: { total_amount: DESC } }]');
  109 |       expect(live.match(/first:\s*20/g) || []).toHaveLength(1);
  110 |     }
  111 |     // Sankey is the default view; Table brings the list back.
  112 |     await expect(page.locator('#clients .money-flow-chart canvas')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  113 |     await page.locator('#clients .mf-view-btn[data-view="table"]').click();
  114 |     await expect(page.locator('#clients .leaf-relationship-list')).toBeVisible();
  115 |     await page.locator('#clients .mf-view-btn[data-view="sankey"]').click();
  116 |   });
  117 | 
  118 |   test('block detail renders voter data and bounded geography links', async ({ page, request }) => {
  119 |     await skipIfPremiumGated(request, '/blocks/');
  120 |     const bodies = await openLeafDetail(page, samples.block);
  121 |     const detail = bodies.find(body => body.includes('BlockDetail')) || '';
  122 |     expect(detail.match(/first:\s*20/g) || []).toHaveLength(2);
  123 |     await expect(page.locator('#registration')).toContainText('Registered');
  124 |     await expect(page.locator('#geography a').first()).toBeVisible();
  125 |   });
  126 | 
  127 |   test('block map links (/blocks/<geoid>/) resolve to the block by its derived id', async ({ page, request }) => {
  128 |     await skipIfPremiumGated(request, '/blocks/');
  129 |     const bodies = await openLeafDetail(page, { ...samples.block, path: '/blocks/461030114001020/' });
  130 |     const detail = bodies.find(body => body.includes('BlockDetail')) || '';
  131 |     expect(detail).toContain('a2e648d3-3cd7-613b-1bfe-85ea19fad69a');
  132 |     await expect(page.locator('#overview')).toContainText('461030114001020');
  133 |   });
  134 | 
  135 |   test('precinct detail bootstraps directly and renders results', async ({ page }) => {
  136 |     const bodies = await openLeafDetail(page, samples.precinct);
  137 |     const detail = bodies.find(body => body.includes('PrecinctDetail')) || '';
  138 |     expect(detail.match(/first:\s*20/g) || []).toHaveLength(1);
  139 |     await expect(page.locator('#results')).toContainText('Democratic');
  140 |     await expect(page.locator('#results')).toContainText('D+51.7%');
  141 |     await expect(page.locator('#county a').first()).toBeVisible();
  142 |   });
  143 | 
  144 |   test('tract detail remains client-rendered under the generalized leaf route', async ({ page }) => {
  145 |     const graphqlBodies: string[] = [];
  146 |     page.on('request', request => {
  147 |       if (request.method() === 'POST' && request.url().includes('/api/graphql')) {
  148 |         graphqlBodies.push(request.postData() || '');
  149 |       }
  150 |     });
  151 | 
  152 |     const response = await page.goto('/tracts/48453000101/');
```