# 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/voter-cycles.spec.ts >> voters and the cycle facet (sites#355) >> without a cycle, the primary filter means any cycle and no cycle is sent
- Location: tests/ui/voter-cycles.spec.ts:62:7

# Error details

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

Locator: locator('#entity-search-results-list')
Expected substring: "SMITH, JANE"
Timeout: 15000ms
Error: element(s) not found

Call log:
  - Expect "toContainText" locator('#entity-search-results-list') with timeout 15000ms
  - waiting for locator('#entity-search-results-list')

```

```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.eyJpc3MiOiJnb2F1dGhlbnRpay5pby9vdXRwb3N0L0tJNUJlOHNjTFZlWk43bjV6WThwNXM4VmJweXJhZE9TS1F2bzlUMmMiLCJzaWQiOiJZNUJUQ1FDMlFTN1JaQUxXU0pNUVRGU0pJSU5UT0c0T1c0UFhLUU4zM0FMMkhGMkRCN0JBIiwic3RhdGUiOiJpRGY0MGYzWDFGZFJzLXYtaE10OXFkNlRBNnUtd25uOXp1RUpEQUpBZXZRIiwicmVkaXJlY3QiOiJodHRwczovL3VzLmVsZWN0LmluZm8vdm90ZXJzLz9mX3ByaW1hcnk9UkVQIn0.zV1Tx2MCZmUgtYQxg8w0J0jqRiA0_zBPwA1uhwpvNv0&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.eyJpc3MiOiJnb2F1dGhlbnRpay5pby9vdXRwb3N0L0tJNUJlOHNjTFZlWk43bjV6WThwNXM4VmJweXJhZE9TS1F2bzlUMmMiLCJzaWQiOiJZNUJUQ1FDMlFTN1JaQUxXU0pNUVRGU0pJSU5UT0c0T1c0UFhLUU4zM0FMMkhGMkRCN0JBIiwic3RhdGUiOiJpRGY0MGYzWDFGZFJzLXYtaE10OXFkNlRBNnUtd25uOXp1RUpEQUpBZXZRIiwicmVkaXJlY3QiOiJodHRwczovL3VzLmVsZWN0LmluZm8vdm90ZXJzLz9mX3ByaW1hcnk9UkVQIn0.zV1Tx2MCZmUgtYQxg8w0J0jqRiA0_zBPwA1uhwpvNv0
    - 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.eyJpc3MiOiJnb2F1dGhlbnRpay5pby9vdXRwb3N0L0tJNUJlOHNjTFZlWk43bjV6WThwNXM4VmJweXJhZE9TS1F2bzlUMmMiLCJzaWQiOiJZNUJUQ1FDMlFTN1JaQUxXU0pNUVRGU0pJSU5UT0c0T1c0UFhLUU4zM0FMMkhGMkRCN0JBIiwic3RhdGUiOiJpRGY0MGYzWDFGZFJzLXYtaE10OXFkNlRBNnUtd25uOXp1RUpEQUpBZXZRIiwicmVkaXJlY3QiOiJodHRwczovL3VzLmVsZWN0LmluZm8vdm90ZXJzLz9mX3ByaW1hcnk9UkVQIn0.zV1Tx2MCZmUgtYQxg8w0J0jqRiA0_zBPwA1uhwpvNv0&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.eyJpc3MiOiJnb2F1dGhlbnRpay5pby9vdXRwb3N0L0tJNUJlOHNjTFZlWk43bjV6WThwNXM4VmJweXJhZE9TS1F2bzlUMmMiLCJzaWQiOiJZNUJUQ1FDMlFTN1JaQUxXU0pNUVRGU0pJSU5UT0c0T1c0UFhLUU4zM0FMMkhGMkRCN0JBIiwic3RhdGUiOiJpRGY0MGYzWDFGZFJzLXYtaE10OXFkNlRBNnUtd25uOXp1RUpEQUpBZXZRIiwicmVkaXJlY3QiOiJodHRwczovL3VzLmVsZWN0LmluZm8vdm90ZXJzLz9mX3ByaW1hcnk9UkVQIn0.zV1Tx2MCZmUgtYQxg8w0J0jqRiA0_zBPwA1uhwpvNv0
```

# Test source

```ts
  1   | import { test, expect, type Page, type Route } from '@playwright/test';
  2   | 
  3   | // Voters and the cycle facet (sites#355, rundeck#846). The voters index gains
  4   | // integer cycle arrays; the voters page sends ?cycle= and the voting-history
  5   | // filters, the homepage shows voters in the cycle, and an index built before
  6   | // the arrays (CYCLE_UNAVAILABLE) falls back to all cycles. GraphQL is mocked,
  7   | // so these run on CI pods and on preview alike.
  8   | 
  9   | const VOTER = {
  10  |   id: 'v-1', type: 'Voter', name: 'SMITH, JANE', score: 12, state: 'TX', city: 'AUSTIN', party: 'DEM',
  11  |   office: null, district: '37', metadata: JSON.stringify({ age: 44, zip: '78701' }), affinityDem: 0, affinityRep: 0,
  12  | };
  13  | 
  14  | interface Request { query: string; variables: Record<string, unknown> }
  15  | 
  16  | async function mockVoters(page: Page, options: { unavailable?: boolean } = {}): Promise<Request[]> {
  17  |   const requests: Request[] = [];
  18  |   await page.route('**/voters/api/graphql', async (route: Route) => {
  19  |     const body = route.request().postDataJSON() as Request;
  20  |     requests.push(body);
  21  |     if (body.query.includes('entityFacets')) return route.fulfill({ json: { data: { entityFacets: [] } } });
  22  |     if (options.unavailable && body.variables.cycle !== undefined) {
  23  |       return route.fulfill({ json: { data: null, errors: [{ message: 'Cycle history is not indexed for these records yet.', extensions: { code: 'CYCLE_UNAVAILABLE' } }] } });
  24  |     }
  25  |     const key = body.query.includes('searchEntities') ? 'searchEntities' : 'browseEntities';
  26  |     return route.fulfill({ json: { data: { [key]: { results: [VOTER], total: 1, page: 1, pageSize: 50, hasMore: false } } } });
  27  |   });
  28  |   return requests;
  29  | }
  30  | 
  31  | const lists = (requests: Request[]) => requests.filter(r => !r.query.includes('entityFacets'));
  32  | 
  33  | test.describe('voters and the cycle facet (sites#355)', () => {
  34  |   test('a cycle scopes the voter list; the page offers the cycle and voting-history filters', async ({ page }) => {
  35  |     const requests = await mockVoters(page);
  36  |     await page.goto('/voters/?cycle=2024');
  37  |     await expect(page.locator('#entity-search-results-list')).toContainText('SMITH, JANE', { timeout: 15000 });
  38  |     expect(lists(requests)[0].variables.cycle).toBe(2024);
  39  |     await expect(page.locator('.ei-cycle-notice')).toContainText('voted in the 2023–2024 cycle');
  40  | 
  41  |     const cycle = page.locator('#entity-filter-cycle');
  42  |     await expect(cycle).toHaveValue('2024');
  43  |     await expect(cycle.locator('option').last()).toHaveText('2011–2012');
  44  |     await expect(page.locator('label[for="entity-filter-general"]')).toHaveText('Voted in the general (2023–2024)');
  45  | 
  46  |     await page.locator('#entity-filter-general').selectOption('yes');
  47  |     await expect.poll(() => JSON.stringify(lists(requests).at(-1)?.variables.filters)).toContain('"general"');
  48  |     expect(lists(requests).at(-1)!.variables).toMatchObject({ cycle: 2024, filters: [{ field: 'general', values: ['yes'] }] });
  49  |     await expect(page).toHaveURL(/f_general=yes/);
  50  | 
  51  |     // The cycle selector is the page's ?cycle=: choosing one reloads in that cycle, keeping the filters.
  52  |     await cycle.selectOption('2022');
  53  |     await expect(page).toHaveURL(/cycle=2022/);
  54  |     await expect(page).toHaveURL(/f_general=yes/);
  55  |     await expect(page.locator('label[for="entity-filter-general"]')).toHaveText('Voted in the general (2021–2022)');
  56  |     await expect.poll(() => lists(requests).at(-1)?.variables.cycle).toBe(2022);
  57  |     await page.locator('#entity-filter-cycle').selectOption('');
  58  |     await expect(page).not.toHaveURL(/cycle=/);
  59  |     await expect(page.locator('label[for="entity-filter-general"]')).toHaveText('Voted in the general (any cycle)');
  60  |   });
  61  | 
  62  |   test('without a cycle, the primary filter means any cycle and no cycle is sent', async ({ page }) => {
  63  |     const requests = await mockVoters(page);
  64  |     await page.goto('/voters/?f_primary=REP');
> 65  |     await expect(page.locator('#entity-search-results-list')).toContainText('SMITH, JANE', { timeout: 15000 });
      |                                                               ^ Error: expect(locator).toContainText(expected) failed
  66  |     expect(lists(requests)[0].variables.cycle).toBeUndefined();
  67  |     expect(lists(requests)[0].variables.filters).toEqual([{ field: 'primary', values: ['REP'] }]);
  68  |     await expect(page.locator('.ei-cycle-notice')).toHaveCount(0);
  69  |   });
  70  | 
  71  |   test('an index without cycle history falls back to all cycles, saying so', async ({ page }) => {
  72  |     const requests = await mockVoters(page, { unavailable: true });
  73  |     await page.goto('/voters/?cycle=2024');
  74  |     await expect(page.locator('#entity-search-results-list')).toContainText('SMITH, JANE', { timeout: 15000 });
  75  |     expect(lists(requests).map(r => r.variables.cycle)).toEqual([2024, undefined]);
  76  |     await expect(page.locator('.ei-cycle-notice')).toContainText('voter cycle history is still being indexed');
  77  |   });
  78  | 
  79  |   test('the voter detail page lists voting by cycle when the index has it', async ({ page }) => {
  80  |     await page.route('**/voters/api/graphql', route => route.fulfill({ json: { data: { voter: {
  81  |       id: 'v-1', name_last: 'SMITH', name_first: 'JANE', state: 'TX', total_votes: 12,
  82  |       cycles: [2016, 2020, 2024], general_cycles: [2020, 2024], dem_primary_cycles: [2024], rep_primary_cycles: [],
  83  |     } } } }));
  84  |     await page.goto('/voters/v-1/?cycle=2024');
  85  |     const section = page.locator('#cycles');
  86  |     await expect(section).toContainText('2023–2024: general, Democratic primary', { timeout: 15000 });
  87  |     await expect(section.locator('strong')).toHaveText('2023–2024: general, Democratic primary');
  88  |     await expect(section.locator('li')).toHaveText(['2023–2024: general, Democratic primary', '2019–2020: general', '2015–2016: other elections']);
  89  |   });
  90  | });
  91  | 
  92  | test.describe('homepage voters count in a cycle (sites#355)', () => {
  93  |   const counts = (voters: number | null, allCycles: string[]) => ({ data: { cycleEntityCounts: {
  94  |     counts: { blocks: 1, politicians: 2, committees: 3, contributors: 4, counties: 5, cycles: 1, districts: 6, employers: 7,
  95  |       individuals: 8, offices: 9, parties: 10, places: 11, postalCodes: 12, precincts: 13, states: 14, tracts: 15, vendors: 16, voters },
  96  |     allCycles, pending: [],
  97  |   } } });
  98  | 
  99  |   test('shows voters who voted in the cycle', async ({ page }) => {
  100 |     let lifetime = 0;
  101 |     await page.route('**/api/voters/count', route => { lifetime++; return route.fulfill({ json: { count: 196000000 } }); });
  102 |     await page.route('**/api/graphql', route => route.fulfill({ json: counts(4200000, ['blocks']) }));
  103 |     await page.goto('/?cycle=2024');
  104 |     await expect(page.locator('#count-voters')).toHaveText('4,200,000', { timeout: 15000 });
  105 |     await expect(page.locator('#count-voters + .ei-count-scope')).toHaveCount(0);
  106 |     expect(lifetime).toBe(0);
  107 |   });
  108 | 
  109 |   test('before the voters index has cycles: the all-cycles total, labelled', async ({ page }) => {
  110 |     await page.route('**/api/voters/count', route => route.fulfill({ json: { count: 196000000 } }));
  111 |     await page.route('**/api/graphql', route => route.fulfill({ json: counts(null, ['blocks', 'voters']) }));
  112 |     await page.goto('/?cycle=2024');
  113 |     await expect(page.locator('#count-voters')).toHaveText('196,000,000', { timeout: 15000 });
  114 |     await expect(page.locator('#count-voters + .ei-count-scope')).toHaveText(' (all cycles)');
  115 |   });
  116 | });
  117 | 
```