# 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 >> tract detail remains client-rendered under the generalized leaf route
- Location: tests/ui/leaf-details.spec.ts:136: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.75/tracts/48453000101/", waiting until "load"

```

# Test source

```ts
  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);
  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, baseURL }) => {
  71  |     await premiumVisitor(page.context(), baseURL); // donor records are premium (sites#277)
  72  |     const bodies = await openLeafDetail(page, samples.individual);
  73  |     const detail = bodies.find(body => body.includes('IndividualDetail')) || '';
  74  |     expect(detail).not.toContain('contributedToCommitteesConnection');
  75  |     expect(detail.match(/first:\s*20/g) || []).toHaveLength(3);
  76  |     expect(bodies.some(body => body.includes('query IndividualCommittees(') && body.includes('moneyCounterparties'))).toBe(true);
  77  |     const legacy = bodies.find(body => body.includes('IndividualCommitteesLegacy'));
  78  |     if (legacy) expect(legacy).toContain('sort: [{ edge: { total_amount: DESC } }]');
  79  |     await expect(page.locator('#committees .leaf-relationship-list')).toBeVisible();
  80  |     await expect(page.locator('#committees .leaf-list-footer')).toContainText(/Showing (top \d+ of [\d,]+|all [\d,]+) committees/);
  81  |   });
  82  | 
  83  |   test('employer detail renders totals and a bounded donor list below threshold', async ({ page, baseURL }) => {
  84  |     await premiumVisitor(page.context(), baseURL); // donor records are premium (sites#277)
  85  |     const bodies = await openLeafDetail(page, samples.employer);
  86  |     const donors = bodies.find(body => body.includes('EmployerDonors')) || '';
  87  |     expect(donors.match(/first:\s*20/g) || []).toHaveLength(1);
  88  |     await expect(page.locator('#activity')).toContainText('Employee donors');
  89  |     await expect(page.locator('#donors .leaf-relationship-list')).toBeVisible();
  90  |   });
  91  | 
  92  |   test('vendor detail renders totals and committee clients, ranked or edge-sorted', async ({ page }) => {
  93  |     const bodies = await openLeafDetail(page, samples.vendor, false);
  94  |     await expect(page.locator('#clients .leaf-list-footer')).toContainText(/Showing (top \d+ of [\d,]+|all [\d,]+) committees/, {
  95  |       timeout: DATA_LOAD_TIMEOUT,
  96  |     });
  97  |     await expect(page.locator('#clients .leaf-relationship-list')).toHaveCount(1);
  98  |     // The ranked list (sites#261) replaces the live list once the graph has PAYS ranks; until then
  99  |     // moneyCounterparties (sites#194), or on an API without it the bounded edge-sorted connection.
  100 |     const live = bodies.find(body => body.includes('query VendorClients('));
  101 |     if (live) {
  102 |       expect(live).toContain('sort: [{ edge: { total_amount: DESC } }]');
  103 |       expect(live.match(/first:\s*20/g) || []).toHaveLength(1);
  104 |     }
  105 |     // Sankey is the default view; Table brings the list back.
  106 |     await expect(page.locator('#clients .money-flow-chart canvas')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  107 |     await page.locator('#clients .mf-view-btn[data-view="table"]').click();
  108 |     await expect(page.locator('#clients .leaf-relationship-list')).toBeVisible();
  109 |     await page.locator('#clients .mf-view-btn[data-view="sankey"]').click();
  110 |   });
  111 | 
  112 |   test('block detail renders voter data and bounded geography links', async ({ page }) => {
  113 |     const bodies = await openLeafDetail(page, samples.block);
  114 |     const detail = bodies.find(body => body.includes('BlockDetail')) || '';
  115 |     expect(detail.match(/first:\s*20/g) || []).toHaveLength(2);
  116 |     await expect(page.locator('#registration')).toContainText('Registered');
  117 |     await expect(page.locator('#geography a').first()).toBeVisible();
  118 |   });
  119 | 
  120 |   test('block map links (/blocks/<geoid>/) resolve to the block by its derived id', async ({ page }) => {
  121 |     const bodies = await openLeafDetail(page, { ...samples.block, path: '/blocks/461030114001020/' });
  122 |     const detail = bodies.find(body => body.includes('BlockDetail')) || '';
  123 |     expect(detail).toContain('a2e648d3-3cd7-613b-1bfe-85ea19fad69a');
  124 |     await expect(page.locator('#overview')).toContainText('461030114001020');
  125 |   });
  126 | 
  127 |   test('precinct detail bootstraps directly and renders results', async ({ page }) => {
  128 |     const bodies = await openLeafDetail(page, samples.precinct);
  129 |     const detail = bodies.find(body => body.includes('PrecinctDetail')) || '';
  130 |     expect(detail.match(/first:\s*20/g) || []).toHaveLength(1);
  131 |     await expect(page.locator('#results')).toContainText('Democratic');
  132 |     await expect(page.locator('#results')).toContainText('D+51.7%');
  133 |     await expect(page.locator('#county a').first()).toBeVisible();
  134 |   });
  135 | 
  136 |   test('tract detail remains client-rendered under the generalized leaf route', async ({ page }) => {
  137 |     const graphqlBodies: string[] = [];
  138 |     page.on('request', request => {
  139 |       if (request.method() === 'POST' && request.url().includes('/api/graphql')) {
  140 |         graphqlBodies.push(request.postData() || '');
  141 |       }
  142 |     });
  143 | 
> 144 |     const response = await page.goto('/tracts/48453000101/');
      |                                 ^ Error: page.goto: net::ERR_ABORTED; maybe frame was detached?
  145 |     expect(response?.status()).toBe(200);
  146 |     await expect(page.locator('header h1')).toContainText('Census Tract 1.01', { timeout: DATA_LOAD_TIMEOUT });
  147 |     await expect(page).toHaveTitle(/elect\.info$/i);
  148 |     await expect(page.locator('meta[name="robots"]')).toHaveAttribute('content', 'noindex, follow');
  149 |     await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href', /^https:\/\/us\.elect\.info\/tracts\//);
  150 | 
  151 |     const detail = graphqlBodies.find(body => body.includes('TractDetail')) || '';
  152 |     expect(detail).toContain('variables');
  153 |     expect(detail.match(/limit:\s*20/g) || []).toHaveLength(3);
  154 |   });
  155 | 
  156 |   test('hub employer renders totals without issuing a live connection query', async ({ page }) => {
  157 |     const graphqlBodies: string[] = [];
  158 |     const resolveUrls: string[] = [];
  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 |       if (body.includes('VendorPurposeFlow')) {
  228 |         await route.fulfill({ json: { data: { vendorPurposeFlow: {
  229 |           taxonomyVersion: null, totalAmount: 0, groups: [], links: [], otherLinks: [],
  230 |         } } } });
  231 |         return;
  232 |       }
  233 |       await route.fulfill({
  234 |         status: 200,
  235 |         contentType: 'application/json',
  236 |         body: JSON.stringify({
  237 |           data: {
  238 |             vendors: id === 'hub-vendor-id' ? [{
  239 |               id: 'hub-vendor-id',
  240 |               name: 'HUB VENDOR',
  241 |               name_normalized: 'hub vendor',
  242 |               city: 'AUSTIN',
  243 |               state: 'TX',
  244 |               total_received: 987654321,
```