# 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/cycle-index.spec.ts >> cycle-index URL contract (sites#194) >> homepage uses the fixed cycle-count document only for a supported cycle
- Location: tests/ui/cycle-index.spec.ts:45: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/?cycle=2024", waiting until "load"

```

# Test source

```ts
  1   | import { test, expect, type Page } from '@playwright/test';
  2   | import { createHash } from 'node:crypto';
  3   | import { skipIfPremiumGated } from './helpers';
  4   | 
  5   | type GraphQLRequest = { query: string; variables?: Record<string, unknown> };
  6   | 
  7   | const legacyCountFields = [
  8   |   'blocks', 'politicians', 'committees', 'counties', 'cycles', 'districts',
  9   |   'employers', 'individuals', 'offices', 'parties', 'places', 'postalCodes', 'precincts',
  10  |   'states', 'tracts', 'vendors',
  11  | ];
  12  | 
  13  | /** Keep the browser/API boundary deterministic: these UI contract tests never call production. */
  14  | async function mockAnonymousGraphQL(page: Page, onRequest: (request: GraphQLRequest) => unknown = () => ({})) {
  15  |   const requests: GraphQLRequest[] = [];
  16  |   await page.route('**/auth/whoami*', route => route.fulfill({ json: { enabled: 'false', email: '', elevated: false, entitlements: {} } }));
  17  |   await page.route('**/auth/account/me*', route => route.fulfill({ json: { email: '', requests: [], agreement: null, granted: [] } }));
  18  |   await page.route('**/api/token', route => route.fulfill({ status: 204 }));
  19  |   await page.route('**/api/cms/**', route => route.fulfill({ json: { totalDocs: 3, docs: [] } }));
  20  |   await page.route('**/api/voters/count', route => route.fulfill({ json: { count: 123 } }));
  21  |   await page.route('**/api/graphql', async route => {
  22  |     const request = route.request().postDataJSON() as GraphQLRequest;
  23  |     requests.push(request);
  24  |     const data = onRequest(request) ?? {};
  25  |     await route.fulfill({ json: { data } });
  26  |   });
  27  |   return requests;
  28  | }
  29  | 
  30  | function entityResponse(request: GraphQLRequest) {
  31  |   if (request.query.includes('entityFacets(')) return { entityFacets: [{ value: '001', label: 'Travis', count: 1 }] };
  32  |   const field = request.query.includes('searchEntities(') ? 'searchEntities' : 'browseEntities';
  33  |   return {
  34  |     [field]: {
  35  |       results: [{ id: 'fixture-1', type: 'Politician', name: 'Test Candidate', score: 1, state: 'TX', city: 'Austin', party: 'DEM', office: 'President', district: null, metadata: '{}', affinityDem: 0, affinityRep: 0 }],
  36  |       total: 51,
  37  |       page: Number(request.variables?.page ?? 1),
  38  |       pageSize: 50,
  39  |       hasMore: true,
  40  |     },
  41  |   };
  42  | }
  43  | 
  44  | test.describe('cycle-index URL contract (sites#194)', () => {
  45  |   test('homepage uses the fixed cycle-count document only for a supported cycle', async ({ page }) => {
  46  |     const requests = await mockAnonymousGraphQL(page, request => {
  47  |       if (request.query.includes('cycleEntityCounts')) {
  48  |         return { cycleEntityCounts: { counts: Object.fromEntries(legacyCountFields.map(field => [field, 7])), allCycles: ['blocks', 'counties', 'places', 'postalCodes', 'precincts', 'tracts'] } };
  49  |       }
  50  |       return { entityCounts: Object.fromEntries(legacyCountFields.map(field => [field, 70])) };
  51  |     });
  52  | 
> 53  |     await page.goto('/?cycle=2024');
      |                ^ Error: page.goto: net::ERR_ABORTED; maybe frame was detached?
  54  |     await expect(page.locator('.ei-cycle-notice')).toContainText(/2024/);
  55  |     await expect.poll(() => requests.length).toBeGreaterThan(0);
  56  |     const countRequest = requests.find(request => request.query.includes('cycleEntityCounts'));
  57  |     expect(countRequest).toBeTruthy();
  58  |     expect(countRequest!.query).toMatch(/query CycleEntityCounts\(\$cycle:\s*Int!\)/);
  59  |     await expect(page.locator('#count-politicians')).toHaveText('7');
  60  |     await expect(page.locator('#count-counties + .ei-count-scope')).toContainText('all cycles');
  61  |     await expect(page.locator('#count-politicians + .ei-count-scope')).toHaveCount(0);
  62  |     await expect(page.locator('#count-voters + .ei-count-scope')).toContainText('all cycles');
  63  |     expect(countRequest!.variables).toEqual({ cycle: 2024 });
  64  |     for (const field of legacyCountFields) expect(countRequest!.query).toContain(field);
  65  |   });
  66  | 
  67  |   for (const query of ['', '?cycle=2023', '?cycle=invalid']) {
  68  |     test(`homepage leaves the legacy anonymous counts query unchanged (${query || 'no cycle'})`, async ({ page }) => {
  69  |       const requests = await mockAnonymousGraphQL(page, () => ({ entityCounts: Object.fromEntries(legacyCountFields.map(field => [field, 12])) }));
  70  |       await page.goto(`/${query}`);
  71  |       await expect(page.locator('#count-politicians')).toHaveText('12');
  72  |       const countRequest = requests.find(request => request.query.includes('entityCounts'));
  73  |       expect(countRequest).toBeTruthy();
  74  |       expect(countRequest!.query).toMatch(/^\s*query\s*\{/);
  75  |       expect(countRequest!.query).toContain('entityCounts');
  76  |       expect(countRequest!.variables).toBeUndefined();
  77  |       expect(countRequest!.query).not.toContain('CycleEntityCounts');
  78  |       await expect(page.locator('.ei-cycle-notice')).toHaveCount(0);
  79  |       await expect(page.locator('.ei-count-scope')).toHaveCount(0);
  80  |     });
  81  |   }
  82  | 
  83  |   test('supported candidate and committee browse/search requests carry cycle and retain URL state', async ({ page }) => {
  84  |     const requests = await mockAnonymousGraphQL(page, entityResponse);
  85  |     await page.goto('/candidates/?cycle=2024&page=2&f_state=TX');
  86  |     await expect(page.locator('.ei-cycle-notice')).toContainText(/2024/);
  87  |     await expect(page.locator('.entity-search-result').first()).toBeVisible();
  88  | 
  89  |     const candidateBrowse = requests.find(request => request.query.includes('browseEntities('));
  90  |     expect(candidateBrowse).toBeTruthy();
  91  |     expect(candidateBrowse!.query).toContain('query CycleBrowseEntities');
  92  |     expect(candidateBrowse!.variables?.cycle).toBe(2024);
  93  |     expect(candidateBrowse!.variables?.page).toBe(2);
  94  |     await expect(page.locator('#entity-results-count')).toHaveText('51 matches');
  95  |     await page.getByRole('button', { name: 'Next', exact: true }).click();
  96  |     await expect(page).toHaveURL(/page=3/);
  97  |     await expect.poll(() => requests.filter(r => r.variables?.page === 3).length).toBe(1);
  98  |     expect(requests.at(-1)!.variables?.cycle).toBe(2024);
  99  | 
  100 |     await page.locator('#entity-search-input').fill('test');
  101 |     await page.locator('#entity-search-form button[type="submit"]').click();
  102 |     await expect.poll(() => requests.some(request => request.query.includes('searchEntities('))).toBe(true);
  103 |     const candidateSearch = requests.find(request => request.query.includes('searchEntities('));
  104 |     expect(candidateSearch!.query).toContain('query CycleSearchEntities');
  105 |     expect(candidateSearch!.variables?.cycle).toBe(2024);
  106 |     await expect(page).toHaveURL(/cycle=2024/);
  107 |     await expect(page).toHaveURL(/f_state=TX/);
  108 |     await expect(page).toHaveURL(/q=test/);
  109 | 
  110 |     await page.goto('/committees/?cycle=2024');
  111 |     await expect(page.locator('.entity-search-result').first()).toBeVisible();
  112 |     const committeeBrowse = requests.filter(request => request.query.includes('browseEntities(')).at(-1);
  113 |     expect(committeeBrowse!.query).toContain('query CycleBrowseEntities');
  114 |     expect(committeeBrowse!.variables?.cycle).toBe(2024);
  115 |     expect(committeeBrowse!.variables?.entityType).toBe('Committee');
  116 |   });
  117 | 
  118 |   test('supported index types scope results; unsupported county counts stay lifetime and explain scope', async ({ page }) => {
  119 |     const requests = await mockAnonymousGraphQL(page, request => request.query.includes('Entities(') ? entityResponse(request) : request.query.includes('query Counties') ? { counties: [], countiesConnection: { totalCount: 0 } } : { states: [{ name: 'Texas', state: 'TX' }] });
  120 |     await page.goto('/parties/?cycle=2024');
  121 |     await expect(page.locator('.ei-cycle-notice')).toContainText(/2024/);
  122 |     await expect(page.locator('.entity-search-result').first()).toBeVisible();
  123 |     const parties = requests.find(request => request.query.includes('browseEntities('));
  124 |     expect(parties!.variables?.cycle).toBe(2024);
  125 |     expect(parties!.query).toContain('CycleBrowseEntities');
  126 | 
  127 |     await page.goto('/counties/?cycle=2024');
  128 |     await expect(page.locator('.ei-cycle-notice')).toContainText(/all cycles/i);
  129 |     await page.getByRole('button', { name: 'List', exact: true }).click();
  130 |     await expect.poll(() => requests.some(request => request.query.includes('query Counties'))).toBe(true);
  131 |     const counties = requests.filter(request => request.query.includes('query Counties')).at(-1);
  132 |     expect(counties).toBeTruthy();
  133 |     expect(counties!.query).not.toContain('Cycle');
  134 |     expect(counties!.variables?.cycle).toBeUndefined();
  135 |   });
  136 | 
  137 |   test('individual global cycle facet is passed to the API independently of lifetime/profile data', async ({ page, request }) => {
  138 |     await skipIfPremiumGated(request, '/individuals/');
  139 |     const requests = await mockAnonymousGraphQL(page, entityResponse);
  140 |     await page.goto('/individuals/?cycle=2024&f_state=TX');
  141 |     await expect(page.locator('.ei-cycle-notice')).toContainText(/2024.*all cycles|all cycles.*2024/i);
  142 |     await expect(page.locator('.entity-search-result').first()).toBeVisible();
  143 |     const browse = requests.find(request => request.query.includes('browseEntities('));
  144 |     expect(browse!.variables?.cycle).toBe(2024);
  145 |     expect(browse!.query).toContain('CycleBrowseEntities');
  146 |     await expect(page.locator('.entity-results-count, #entity-results-count')).toBeVisible();
  147 |     await expect.poll(() => requests.some(request => request.query.includes('CycleEntityFacets'))).toBe(true);
  148 |     const facets = requests.filter(request => request.query.includes('CycleEntityFacets'));
  149 |     for (const facet of facets) expect(facet.variables?.cycle).toBe(2024);
  150 |     expect(browse!.variables?.filters).toContainEqual({ field: 'state', values: ['TX'] });
  151 |   });
  152 | });
  153 | 
```