# 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) >> individual global cycle facet is passed to the API independently of lifetime/profile data
- Location: tests/ui/cycle-index.spec.ts:136:7

# Error details

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

Locator: locator('.ei-cycle-notice')
Expected pattern: /2024.*all cycles|all cycles.*2024/i
Timeout: 5000ms
Error: element(s) not found

Call log:
  - Expect "toContainText" locator('.ei-cycle-notice') with timeout 5000ms
  - waiting for locator('.ei-cycle-notice')

```

```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.eyJpc3MiOiJnb2F1dGhlbnRpay5pby9vdXRwb3N0L0tJNUJlOHNjTFZlWk43bjV6WThwNXM4VmJweXJhZE9TS1F2bzlUMmMiLCJzaWQiOiJPU1dLRzZWVFI3SDVTNktCUlBGUU80S1RBRDNNV05FSkVOUFdIT1lRMzVVUFc2TlZXSEZBIiwic3RhdGUiOiI2MWJ4Vk9ydUpaMXkxN0VYSndNMGo4Q1ZtZFlJYzNkcVFTQ3U1VW5pT0drIiwicmVkaXJlY3QiOiJodHRwczovL3VzLmVsZWN0LmluZm8vaW5kaXZpZHVhbHMvP2N5Y2xlPTIwMjRcdTAwMjZmX3N0YXRlPVRYIn0.4updWaje4KWPOGAIQGR39_ixPVdMYEN7k0DSoIcABls&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.eyJpc3MiOiJnb2F1dGhlbnRpay5pby9vdXRwb3N0L0tJNUJlOHNjTFZlWk43bjV6WThwNXM4VmJweXJhZE9TS1F2bzlUMmMiLCJzaWQiOiJPU1dLRzZWVFI3SDVTNktCUlBGUU80S1RBRDNNV05FSkVOUFdIT1lRMzVVUFc2TlZXSEZBIiwic3RhdGUiOiI2MWJ4Vk9ydUpaMXkxN0VYSndNMGo4Q1ZtZFlJYzNkcVFTQ3U1VW5pT0drIiwicmVkaXJlY3QiOiJodHRwczovL3VzLmVsZWN0LmluZm8vaW5kaXZpZHVhbHMvP2N5Y2xlPTIwMjRcdTAwMjZmX3N0YXRlPVRYIn0.4updWaje4KWPOGAIQGR39_ixPVdMYEN7k0DSoIcABls
    - 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.eyJpc3MiOiJnb2F1dGhlbnRpay5pby9vdXRwb3N0L0tJNUJlOHNjTFZlWk43bjV6WThwNXM4VmJweXJhZE9TS1F2bzlUMmMiLCJzaWQiOiJPU1dLRzZWVFI3SDVTNktCUlBGUU80S1RBRDNNV05FSkVOUFdIT1lRMzVVUFc2TlZXSEZBIiwic3RhdGUiOiI2MWJ4Vk9ydUpaMXkxN0VYSndNMGo4Q1ZtZFlJYzNkcVFTQ3U1VW5pT0drIiwicmVkaXJlY3QiOiJodHRwczovL3VzLmVsZWN0LmluZm8vaW5kaXZpZHVhbHMvP2N5Y2xlPTIwMjRcdTAwMjZmX3N0YXRlPVRYIn0.4updWaje4KWPOGAIQGR39_ixPVdMYEN7k0DSoIcABls&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.eyJpc3MiOiJnb2F1dGhlbnRpay5pby9vdXRwb3N0L0tJNUJlOHNjTFZlWk43bjV6WThwNXM4VmJweXJhZE9TS1F2bzlUMmMiLCJzaWQiOiJPU1dLRzZWVFI3SDVTNktCUlBGUU80S1RBRDNNV05FSkVOUFdIT1lRMzVVUFc2TlZXSEZBIiwic3RhdGUiOiI2MWJ4Vk9ydUpaMXkxN0VYSndNMGo4Q1ZtZFlJYzNkcVFTQ3U1VW5pT0drIiwicmVkaXJlY3QiOiJodHRwczovL3VzLmVsZWN0LmluZm8vaW5kaXZpZHVhbHMvP2N5Y2xlPTIwMjRcdTAwMjZmX3N0YXRlPVRYIn0.4updWaje4KWPOGAIQGR39_ixPVdMYEN7k0DSoIcABls
```

# Test source

```ts
  39  |     },
  40  |   };
  41  | }
  42  | 
  43  | test.describe('cycle-index URL contract (sites#194)', () => {
  44  |   test('homepage uses the fixed cycle-count document only for a supported cycle', async ({ page }) => {
  45  |     const requests = await mockAnonymousGraphQL(page, request => {
  46  |       if (request.query.includes('cycleEntityCounts')) {
  47  |         return { cycleEntityCounts: { counts: Object.fromEntries(legacyCountFields.map(field => [field, 7])), allCycles: ['blocks', 'contributors', 'counties', 'places', 'postalCodes', 'precincts', 'tracts'] } };
  48  |       }
  49  |       return { entityCounts: Object.fromEntries(legacyCountFields.map(field => [field, 70])) };
  50  |     });
  51  | 
  52  |     await page.goto('/?cycle=2024');
  53  |     await expect(page.locator('.ei-cycle-notice')).toContainText(/2024/);
  54  |     await expect.poll(() => requests.length).toBeGreaterThan(0);
  55  |     const countRequest = requests.find(request => request.query.includes('cycleEntityCounts'));
  56  |     expect(countRequest).toBeTruthy();
  57  |     expect(countRequest!.query).toMatch(/query CycleEntityCounts\(\$cycle:\s*Int!\)/);
  58  |     await expect(page.locator('#count-politicians')).toHaveText('7');
  59  |     await expect(page.locator('#count-counties + .ei-count-scope')).toContainText('all cycles');
  60  |     await expect(page.locator('#count-politicians + .ei-count-scope')).toHaveCount(0);
  61  |     await expect(page.locator('#count-voters + .ei-count-scope')).toContainText('all cycles');
  62  |     expect(countRequest!.variables).toEqual({ cycle: 2024 });
  63  |     for (const field of legacyCountFields) expect(countRequest!.query).toContain(field);
  64  |   });
  65  | 
  66  |   for (const query of ['', '?cycle=2023', '?cycle=invalid']) {
  67  |     test(`homepage leaves the legacy anonymous counts query unchanged (${query || 'no cycle'})`, async ({ page }) => {
  68  |       const requests = await mockAnonymousGraphQL(page, () => ({ entityCounts: Object.fromEntries(legacyCountFields.map(field => [field, 12])) }));
  69  |       await page.goto(`/${query}`);
  70  |       await expect(page.locator('#count-politicians')).toHaveText('12');
  71  |       const countRequest = requests.find(request => request.query.includes('entityCounts'));
  72  |       expect(countRequest).toBeTruthy();
  73  |       expect(countRequest!.query).toMatch(/^\s*query\s*\{/);
  74  |       expect(countRequest!.query).toContain('entityCounts');
  75  |       expect(countRequest!.variables).toBeUndefined();
  76  |       expect(countRequest!.query).not.toContain('CycleEntityCounts');
  77  |       await expect(page.locator('.ei-cycle-notice')).toHaveCount(0);
  78  |       await expect(page.locator('.ei-count-scope')).toHaveCount(0);
  79  |     });
  80  |   }
  81  | 
  82  |   test('supported candidate and committee browse/search requests carry cycle and retain URL state', async ({ page }) => {
  83  |     const requests = await mockAnonymousGraphQL(page, entityResponse);
  84  |     await page.goto('/candidates/?cycle=2024&page=2&f_state=TX');
  85  |     await expect(page.locator('.ei-cycle-notice')).toContainText(/2024/);
  86  |     await expect(page.locator('.entity-search-result').first()).toBeVisible();
  87  | 
  88  |     const candidateBrowse = requests.find(request => request.query.includes('browseEntities('));
  89  |     expect(candidateBrowse).toBeTruthy();
  90  |     expect(candidateBrowse!.query).toContain('query CycleBrowseEntities');
  91  |     expect(candidateBrowse!.variables?.cycle).toBe(2024);
  92  |     expect(candidateBrowse!.variables?.page).toBe(2);
  93  |     await expect(page.locator('#entity-results-count')).toHaveText('51 matches');
  94  |     await page.getByRole('button', { name: 'Next', exact: true }).click();
  95  |     await expect(page).toHaveURL(/page=3/);
  96  |     await expect.poll(() => requests.filter(r => r.variables?.page === 3).length).toBe(1);
  97  |     expect(requests.at(-1)!.variables?.cycle).toBe(2024);
  98  | 
  99  |     await page.locator('#entity-search-input').fill('test');
  100 |     await page.locator('#entity-search-form button[type="submit"]').click();
  101 |     await expect.poll(() => requests.some(request => request.query.includes('searchEntities('))).toBe(true);
  102 |     const candidateSearch = requests.find(request => request.query.includes('searchEntities('));
  103 |     expect(candidateSearch!.query).toContain('query CycleSearchEntities');
  104 |     expect(candidateSearch!.variables?.cycle).toBe(2024);
  105 |     await expect(page).toHaveURL(/cycle=2024/);
  106 |     await expect(page).toHaveURL(/f_state=TX/);
  107 |     await expect(page).toHaveURL(/q=test/);
  108 | 
  109 |     await page.goto('/committees/?cycle=2024');
  110 |     await expect(page.locator('.entity-search-result').first()).toBeVisible();
  111 |     const committeeBrowse = requests.filter(request => request.query.includes('browseEntities(')).at(-1);
  112 |     expect(committeeBrowse!.query).toContain('query CycleBrowseEntities');
  113 |     expect(committeeBrowse!.variables?.cycle).toBe(2024);
  114 |     expect(committeeBrowse!.variables?.entityType).toBe('Committee');
  115 |   });
  116 | 
  117 |   test('supported index types scope results; unsupported county counts stay lifetime and explain scope', async ({ page }) => {
  118 |     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' }] });
  119 |     await page.goto('/parties/?cycle=2024');
  120 |     await expect(page.locator('.ei-cycle-notice')).toContainText(/2024/);
  121 |     await expect(page.locator('.entity-search-result').first()).toBeVisible();
  122 |     const parties = requests.find(request => request.query.includes('browseEntities('));
  123 |     expect(parties!.variables?.cycle).toBe(2024);
  124 |     expect(parties!.query).toContain('CycleBrowseEntities');
  125 | 
  126 |     await page.goto('/counties/?cycle=2024');
  127 |     await expect(page.locator('.ei-cycle-notice')).toContainText(/all cycles/i);
  128 |     await page.getByRole('button', { name: 'List', exact: true }).click();
  129 |     await expect.poll(() => requests.some(request => request.query.includes('query Counties'))).toBe(true);
  130 |     const counties = requests.filter(request => request.query.includes('query Counties')).at(-1);
  131 |     expect(counties).toBeTruthy();
  132 |     expect(counties!.query).not.toContain('Cycle');
  133 |     expect(counties!.variables?.cycle).toBeUndefined();
  134 |   });
  135 | 
  136 |   test('individual global cycle facet is passed to the API independently of lifetime/profile data', async ({ page }) => {
  137 |     const requests = await mockAnonymousGraphQL(page, entityResponse);
  138 |     await page.goto('/individuals/?cycle=2024&f_state=TX');
> 139 |     await expect(page.locator('.ei-cycle-notice')).toContainText(/2024.*all cycles|all cycles.*2024/i);
      |                                                    ^ Error: expect(locator).toContainText(expected) failed
  140 |     await expect(page.locator('.entity-search-result').first()).toBeVisible();
  141 |     const browse = requests.find(request => request.query.includes('browseEntities('));
  142 |     expect(browse!.variables?.cycle).toBe(2024);
  143 |     expect(browse!.query).toContain('CycleBrowseEntities');
  144 |     await expect(page.locator('.entity-results-count, #entity-results-count')).toBeVisible();
  145 |     await expect.poll(() => requests.some(request => request.query.includes('CycleEntityFacets'))).toBe(true);
  146 |     const facets = requests.filter(request => request.query.includes('CycleEntityFacets'));
  147 |     for (const facet of facets) expect(facet.variables?.cycle).toBe(2024);
  148 |     expect(browse!.variables?.filters).toContainEqual({ field: 'state', values: ['TX'] });
  149 |   });
  150 | });
  151 | 
  152 | 
  153 | // Captured from pre-change persisted documents (baseline 1c2ed79). Pin the
  154 | // browser's legacy documents in addition to the API's Cypher fingerprints.
  155 | const legacyBrowseHash = '6cf0d0346d4ad92ee6ac58bb6bdd1c7109e80031f9770a285584fcd11c4beaa3';
  156 | const legacySearchHash = '1cc10eb69065f4bada75f2618cb56f3d2b40bc56d591f5cbd29bf1bd93c2509e';
  157 | const hashDocument = (query: string) => createHash('sha256').update(query.replace(/\s+/g, ' ').trim()).digest('hex');
  158 | 
  159 | for (const [collection, entityType, sortField] of [
  160 |   ['candidates', 'Politician', 'total_contributions'], ['committees', 'Committee', 'total_dollars'],
  161 | ]) {
  162 |   for (const suffix of ['', '?cycle=2023']) {
  163 |     test(`${collection}: legacy requests/rendering unchanged ${suffix || 'without cycle'}`, async ({ page }) => {
  164 |       const requests = await mockAnonymousGraphQL(page, entityResponse);
  165 |       await page.goto(`/${collection}/${suffix}`);
  166 |       await expect(page.locator('.entity-search-result').first()).toBeVisible();
  167 |       const browse = requests.find(r => r.query.includes('browseEntities('))!;
  168 |       expect(hashDocument(browse.query)).toBe(legacyBrowseHash);
  169 |       expect(browse.variables).toEqual({ entityType, page: 1, sortOrder: 'DESC', sortField });
  170 |       await expect(page.locator('.ei-cycle-notice')).toHaveCount(0);
  171 |       await expect(page.locator('#entity-results-count')).toHaveCount(0);
  172 |       await page.locator('#entity-search-input').fill('Smith');
  173 |       await page.locator('#entity-search-form').evaluate(form => (form as HTMLFormElement).requestSubmit());
  174 |       await expect.poll(() => requests.some(r => r.query.includes('searchEntities('))).toBe(true);
  175 |       const search = requests.find(r => r.query.includes('searchEntities('))!;
  176 |       expect(hashDocument(search.query)).toBe(legacySearchHash);
  177 |       expect(search.variables).toEqual({ entityType, page: 1, sortOrder: 'DESC', query: 'Smith', sortField });
  178 |     });
  179 |   }
  180 | }
  181 | 
  182 | const yearWhere = { OR: [
  183 |   { cycles: { eq: '2024' } }, { cycles: { startsWith: '2024,' } },
  184 |   { cycles: { endsWith: ',2024' } }, { cycles: { contains: ',2024,' } },
  185 | ] };
  186 | 
  187 | test('specialized state search and district selector scope their results and counts using exact CSV year tokens', async ({ page }) => {
  188 |   const requests = await mockAnonymousGraphQL(page, request => {
  189 |     if (request.query.includes('browseEntities(') || request.query.includes('searchEntities(')) return entityResponse(request);
  190 |     return { states: [{ id: 'state-1', name: 'Texas', state: 'TX' }], statesConnection: { totalCount: 1 } };
  191 |   });
  192 |   await page.goto('/states/?cycle=2024&q=Tex');
  193 |   await expect(page.locator('.state-result')).toContainText('Texas');
  194 |   await expect(page.locator('.ei-cycle-notice')).toContainText('2024');
  195 |   await expect(page.locator('.state-pagination-info')).toHaveText('Showing all 1 states');
  196 |   const states = requests.find(r => r.query.includes('query CycleStates'))!;
  197 |   expect(states.variables).toEqual({ offset: 0, where: { AND: [yearWhere, { name: { contains: 'Tex' } }] } });
  198 |   await page.goto('/districts/?cycle=2024&state=TX');
  199 |   await expect(page.locator('.entity-search-result').first()).toBeVisible({ timeout: 10000 });
  200 |   await expect(page.locator('.ei-cycle-notice')).toContainText('2024');
  201 |   const districts = requests.find(r => r.query.includes('CycleBrowseEntities') && r.variables?.entityType === 'District')!;
  202 |   expect(districts).toBeTruthy();
  203 |   expect(districts.variables?.cycle).toBe(2024);
  204 |   expect(String(districts.variables?.filter ?? '')).toContain('state:TX');
  205 |   const priorRequests = requests.length;
  206 |   await page.goto('/states/');
  207 |   await page.getByRole('button', { name: 'Grid', exact: true }).click();
  208 |   await expect(page.locator('.state-chip')).toHaveCount(52); // 50 states, DC and PR (rundeck#852)
  209 |   await expect(page.locator('.ei-cycle-notice')).toHaveCount(0);
  210 |   expect(requests.length).toBe(priorRequests); // The original timeless grid makes no GraphQL request.
  211 | });
  212 | 
  213 | test('catalog datasource list explicitly remains all cycles without changing its CMS request', async ({ page }) => {
  214 |   await mockAnonymousGraphQL(page);
  215 |   await page.goto('/datasources/?cycle=2024&q=FEC');
  216 |   await expect(page.locator('.ei-cycle-notice')).toContainText('Data sources cover all cycles');
  217 |   await page.goto('/datasources/?q=FEC');
  218 |   await expect(page.locator('.ei-cycle-notice')).toHaveCount(0);
  219 | });
  220 | 
  221 | test('homepage never waits or displays zero for pending heavy counts, and fills them on the next visit without polling', async ({ page }) => {
  222 |   let warm = false;
  223 |   const requests = await mockAnonymousGraphQL(page, () => ({ cycleEntityCounts: {
  224 |     counts: { ...Object.fromEntries(legacyCountFields.map(field => [field, 7])),
  225 |       individuals: warm ? 5999507 : null, employers: warm ? 929936 : null, vendors: warm ? 156307 : null },
  226 |     allCycles: ['blocks', 'counties'], pending: warm ? [] : ['individuals', 'employers', 'vendors'],
  227 |   } }));
  228 |   await page.goto('/?cycle=2024');
  229 |   await expect(page.locator('#count-politicians')).toHaveText('7');
  230 |   for (const field of ['individuals', 'employers', 'vendors']) {
  231 |     await expect(page.locator(`#count-${field}`)).toHaveText('…');
  232 |     await expect(page.locator(`#count-${field}`)).toHaveAttribute('title', /background/);
  233 |   }
  234 |   await expect(page.locator('.ei-cycle-notice')).toContainText('revisit');
  235 |   // One counts document and one voter-count document (sites#355) per visit: no polling.
  236 |   const counted = (operation: string) => requests.filter(r => r.query.includes(`query ${operation}(`)).length;
  237 |   expect(counted('CycleEntityCounts')).toBe(1);
  238 |   expect(counted('CycleVoterCount')).toBe(1);
  239 |   warm = true;
```