# 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/voters.spec.ts >> voters (premium) >> search needs every term: name plus city
- Location: tests/ui/voters.spec.ts:91:7

# Error details

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

```
Error: page.fill: Test timeout of 30000ms exceeded.
Call log:
  - waiting for locator('#entity-search-input')

```

# Page snapshot

```yaml
- generic [ref=e1]:
  - generic:
    - region "Status messages"
  - generic [active] [ref=e7]:
    - text: ⋯
    - banner
    - main "Authentication form" [ref=e8]:
      - generic [ref=e9]: elect.info
      - generic [ref=e10]:
        - heading "Sign in to elect.info" [level=1] [ref=e12]
        - generic [ref=e14]:
          - paragraph [ref=e15]: Login to continue to us.elect.info.
          - generic [ref=e16]:
            - generic [ref=e17]: Email
            - textbox "Email" [ref=e19]
          - generic [ref=e20]:
            - generic [ref=e21]: Password
            - textbox "Password" [ref=e26]:
              - /placeholder: Please enter your password
          - button "Log in" [ref=e28] [cursor=pointer]
        - generic [ref=e29]:
          - group "Login sources" [ref=e30]:
            - text: or
            - button "Continue with Google" [ref=e32] [cursor=pointer]
          - group "Additional actions" [ref=e33]:
            - generic [ref=e35]:
              - text: Need an account?
              - link "Sign up." [ref=e36] [cursor=pointer]:
                - /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.eyJpc3MiOiJnb2F1dGhlbnRpay5pby9vdXRwb3N0L0tJNUJlOHNjTFZlWk43bjV6WThwNXM4VmJweXJhZE9TS1F2bzlUMmMiLCJzaWQiOiJIUTNOWE5XSU1XMjdFRlJVTE9NQUhNWlNBV01KSlNYQUtYTUJESjZTV0ZXUUpNRURKNTNBIiwic3RhdGUiOiJiTllGc3Y4LXlaZ0V5NzhTT2luckRxWS1HYVo5TU03RVFOZEdsblN1WHU0IiwicmVkaXJlY3QiOiJodHRwczovL3VzLmVsZWN0LmluZm8vdm90ZXJzLyJ9.eciyoEbPlks8tYZepvg2EDzCQkGrSfry37RC8no51ZM&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.eyJpc3MiOiJnb2F1dGhlbnRpay5pby9vdXRwb3N0L0tJNUJlOHNjTFZlWk43bjV6WThwNXM4VmJweXJhZE9TS1F2bzlUMmMiLCJzaWQiOiJIUTNOWE5XSU1XMjdFRlJVTE9NQUhNWlNBV01KSlNYQUtYTUJESjZTV0ZXUUpNRURKNTNBIiwic3RhdGUiOiJiTllGc3Y4LXlaZ0V5NzhTT2luckRxWS1HYVo5TU03RVFOZEdsblN1WHU0IiwicmVkaXJlY3QiOiJodHRwczovL3VzLmVsZWN0LmluZm8vdm90ZXJzLyJ9.eciyoEbPlks8tYZepvg2EDzCQkGrSfry37RC8no51ZM
            - link "Forgot username or password?" [ref=e38] [cursor=pointer]:
              - /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.eyJpc3MiOiJnb2F1dGhlbnRpay5pby9vdXRwb3N0L0tJNUJlOHNjTFZlWk43bjV6WThwNXM4VmJweXJhZE9TS1F2bzlUMmMiLCJzaWQiOiJIUTNOWE5XSU1XMjdFRlJVTE9NQUhNWlNBV01KSlNYQUtYTUJESjZTV0ZXUUpNRURKNTNBIiwic3RhdGUiOiJiTllGc3Y4LXlaZ0V5NzhTT2luckRxWS1HYVo5TU03RVFOZEdsblN1WHU0IiwicmVkaXJlY3QiOiJodHRwczovL3VzLmVsZWN0LmluZm8vdm90ZXJzLyJ9.eciyoEbPlks8tYZepvg2EDzCQkGrSfry37RC8no51ZM&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.eyJpc3MiOiJnb2F1dGhlbnRpay5pby9vdXRwb3N0L0tJNUJlOHNjTFZlWk43bjV6WThwNXM4VmJweXJhZE9TS1F2bzlUMmMiLCJzaWQiOiJIUTNOWE5XSU1XMjdFRlJVTE9NQUhNWlNBV01KSlNYQUtYTUJESjZTV0ZXUUpNRURKNTNBIiwic3RhdGUiOiJiTllGc3Y4LXlaZ0V5NzhTT2luckRxWS1HYVo5TU03RVFOZEdsblN1WHU0IiwicmVkaXJlY3QiOiJodHRwczovL3VzLmVsZWN0LmluZm8vdm90ZXJzLyJ9.eciyoEbPlks8tYZepvg2EDzCQkGrSfry37RC8no51ZM
```

# Test source

```ts
  1   | import { test, expect, type APIRequestContext, type APIResponse } from '@playwright/test';
  2   | import { mintVisitorToken, visitorHeaders } from './helpers';
  3   | 
  4   | // Voter search and detail pages (electinfo/sites#277), like Individual Donors.
  5   | // Voter records are premium: the pages query /voters/api/graphql (behind the
  6   | // premium gate, marked for the API by nginx), and the public /api/graphql
  7   | // must refuse voter queries to anyone without a premium visitor token.
  8   | 
  9   | const DATA_LOAD_TIMEOUT = 30_000;
  10  | 
  11  | const BROWSE = `query { browseEntities(entityType: Voter, page: 1, pageSize: 1, sortOrder: DESC, sortField: "total_votes") {
  12  |   results { id name } } }`;
  13  | 
  14  | /** The CI test pod proxies to the production API, which serves voters only after the release. */
  15  | async function apiServesVoters(request: APIRequestContext): Promise<boolean> {
  16  |   const res = await request.post('/api/graphql', {
  17  |     data: { query: '{ __type(name: "EntityType") { enumValues { name } } }' },
  18  |     headers: visitorHeaders(),
  19  |   });
  20  |   if (res.status() !== 200) return false;
  21  |   const body = await res.json() as { data?: { __type?: { enumValues?: Array<{ name: string }> } } };
  22  |   return !!body.data?.__type?.enumValues?.some(v => v.name === 'Voter');
  23  | }
  24  | 
  25  | test.describe('voters (premium)', () => {
  26  |   test.beforeEach(async ({ request }) => {
  27  |     test.skip(!(await apiServesVoters(request)), 'the API behind this host does not serve voters yet');
  28  |   });
  29  | 
  30  |   test('the public /api/graphql refuses voter queries', async ({ request }) => {
  31  |     // Refused by the visitor-token gate (401 without a token, 403 for an unlisted
  32  |     // document) or, on a target without the gate, by the API (FORBIDDEN).
  33  |     const refused = async (res: APIResponse) => {
  34  |       if (res.status() === 401 || res.status() === 403) return;
  35  |       const body = await res.json() as { data?: unknown; errors?: Array<{ extensions?: { code?: string } }> };
  36  |       expect(body.errors?.[0]?.extensions?.code).toBe('FORBIDDEN');
  37  |       expect(JSON.stringify(body.data ?? null)).not.toContain('"id"');
  38  |     };
  39  |     const anon = mintVisitorToken('anon');
  40  |     const asAnon = anon ? { Cookie: `ei_vt=${anon}` } : {};
  41  | 
  42  |     await refused(await request.post('/api/graphql', { data: { query: BROWSE }, headers: asAnon }));
  43  |     // A client-sent marker must not get through either: nginx clears it.
  44  |     await refused(await request.post('/api/graphql', {
  45  |       data: { query: BROWSE },
  46  |       headers: { ...asAnon, 'X-Premium-Access': 'voters' },
  47  |     }));
  48  |     // Facets are voter data too.
  49  |     await refused(await request.post('/api/graphql', {
  50  |       data: { query: '{ entityFacets(entityType: Voter, field: "county", filters: [{ field: "state", values: ["FL"] }]) { value count } }' },
  51  |       headers: asAnon,
  52  |     }));
  53  |   });
  54  | 
  55  |   test('a premium visitor token reads voters through /api/graphql', async ({ request }) => {
  56  |     test.skip(!mintVisitorToken('premium'), 'needs the target\'s visitor-token key (EI_VT_KEY)');
  57  |     const res = await request.post('/api/graphql', { data: { query: BROWSE }, headers: visitorHeaders() });
  58  |     expect(res.status()).toBe(200);
  59  |     const body = await res.json() as { data?: { browseEntities?: { results: Array<{ id: string }> } } };
  60  |     expect(body.data?.browseEntities?.results.length).toBe(1);
  61  |   });
  62  | 
  63  |   test('/voters/api/graphql answers voter queries and only POST', async ({ request }) => {
  64  |     const res = await request.post('/voters/api/graphql', { data: { query: BROWSE } });
  65  |     expect(res.status()).toBe(200);
  66  |     expect(res.headers()['cache-control']).toContain('no-store');
  67  |     const body = await res.json() as { data?: { browseEntities?: { results: Array<{ id: string; name: string }> } } };
  68  |     expect(body.data?.browseEntities?.results.length).toBe(1);
  69  | 
  70  |     const get = await request.get('/voters/api/graphql', { maxRedirects: 0 });
  71  |     expect(get.status()).toBe(405);
  72  |   });
  73  | 
  74  |   test('browse lists the most active voters and links to detail pages', async ({ page }) => {
  75  |     const response = await page.goto('/voters/');
  76  |     expect(response?.status()).toBe(200);
  77  |     const results = page.locator('.entity-search-result');
  78  |     await expect(results.first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  79  |     await expect(results.first()).toContainText(/elections voted/);
  80  | 
  81  |     const href = await results.first().getAttribute('href');
  82  |     expect(href).toMatch(/^\/voters\/[0-9a-f-]{36}\/$/);
  83  |     const detail = await page.goto(href!);
  84  |     expect(detail?.status()).toBe(200);
  85  |     await expect(page.locator('.leaf-detail-page h1')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  86  |     await expect(page.locator('.leaf-detail-page h1')).not.toHaveText(/Not Found/i);
  87  |     await expect(page.locator('section#history')).toContainText('Elections voted');
  88  |     await expect(page.locator('meta[name="robots"]')).toHaveAttribute('content', 'noindex, follow');
  89  |   });
  90  | 
  91  |   test('search needs every term: name plus city', async ({ page }) => {
  92  |     await page.goto('/voters/');
> 93  |     await page.fill('#entity-search-input', 'john smith austin');
      |                ^ Error: page.fill: Test timeout of 30000ms exceeded.
  94  |     await page.locator('#entity-search-form button[type="submit"]').click();
  95  |     const results = page.locator('.entity-search-result');
  96  |     await expect(results.first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  97  |     await expect(results.filter({ hasText: /SMITH, JOHN/ }).first()).toContainText(/Austin, TX/i);
  98  |   });
  99  | 
  100 |   test('state filter narrows the list and scopes the county and district dropdowns', async ({ page }) => {
  101 |     await page.goto('/voters/');
  102 |     await expect(page.locator('.entity-search-result').first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  103 |     await expect(page.locator('.entity-filter-control[data-key="county"]')).toBeHidden();
  104 |     await page.selectOption('#entity-filter-state', 'FL');
  105 |     await expect(page.locator('.entity-search-result').first()).toContainText(', FL', { timeout: DATA_LOAD_TIMEOUT });
  106 |     await expect(page).toHaveURL(/[?&]f_state=FL/);
  107 |     await expect(page.locator('#entity-filter-county option', { hasText: /Miami-Dade \([\d,]+\)/ })).toHaveCount(1, { timeout: DATA_LOAD_TIMEOUT });
  108 |     await expect(page.locator('#entity-filter-cd option', { hasText: 'FL-27' })).toHaveCount(1);
  109 |   });
  110 | 
  111 |   test('filters combine, count matches and survive a reload', async ({ page }) => {
  112 |     await page.goto('/voters/?f_state=FL&f_county=MIAMI-DADE&f_age=65-74&f_party=DEM');
  113 |     const results = page.locator('.entity-search-result');
  114 |     await expect(results.first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  115 |     await expect(results.first()).toContainText('Democratic (DEM)');
  116 |     await expect(results.first()).toContainText(/Age (6[5-9]|7[0-4])\b/);
  117 |     await expect(page.locator('#entity-results-count')).toHaveText(/^[\d,]+ match(es)?$/);
  118 |     await expect(page.locator('#entity-results-title')).toContainText('Miami-Dade');
  119 |     await expect(page.locator('#entity-filter-county')).toHaveValue('MIAMI-DADE', { timeout: DATA_LOAD_TIMEOUT });
  120 | 
  121 |     await page.locator('#entity-filter-reset').click();
  122 |     await expect(page).not.toHaveURL(/f_state=/);
  123 |     await expect(page.locator('.entity-filter-control[data-key="county"]')).toBeHidden();
  124 |   });
  125 | 
  126 |   test('race and Civitech score filters (persuadable, high-turnout Latinx voters in Miami-Dade)', async ({ page }) => {
  127 |     await page.goto('/voters/?f_state=FL&f_county=MIAMI-DADE&f_race=LATINX&f_support_dem=tossup&f_turnout_pres=high');
  128 |     await expect(page.locator('.entity-search-result').first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  129 |     await expect(page.locator('#entity-results-count')).toHaveText(/^[\d,]+ match(es)?$/);
  130 |     await expect(page.locator('#entity-results-title')).toContainText('Latinx');
  131 |     await expect(page.locator('#entity-filter-support_dem')).toHaveValue('tossup');
  132 |   });
  133 | 
  134 |   test('every Districts row links; legislative districts open the filtered voters list', async ({ page }) => {
  135 |     // A Rochester, NY voter with all four district codes and a precinct.
  136 |     await page.goto('/voters/0c13135d-8e3d-3aa4-1077-28a81eb5d10f/');
  137 |     const districts = page.locator('section#districts');
  138 |     await expect(districts.locator('a').first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  139 |     const hrefs = await districts.locator('a').evaluateAll(as => as.map(a => a.getAttribute('href')));
  140 |     expect(hrefs).toEqual(expect.arrayContaining([
  141 |       '/districts/ny-25/',
  142 |       '/voters/?f_state=NY&f_cd=NY-25',
  143 |       '/voters/?f_state=NY&f_senate=NY-055',
  144 |       '/voters/?f_state=NY&f_house=NY-138',
  145 |       '/voters/?f_state=NY&f_precinct=36055Rochester_Ward_24_ED_6',
  146 |     ]));
  147 |     await expect(districts.locator('dd')).toHaveCount(4);
  148 |     expect(await districts.locator('dd:not(:has(a))').count()).toBe(0);
  149 | 
  150 |     // Location: state, place, ZIP and county pages (graph ids from the public API) plus the filtered voters list.
  151 |     const location = page.locator('section#location');
  152 |     await expect(location.locator('a[href^="/places/"]')).toHaveText('Rochester city', { timeout: DATA_LOAD_TIMEOUT });
  153 |     const locationHrefs = await location.locator('a').evaluateAll(as => as.map(a => a.getAttribute('href')));
  154 |     expect(locationHrefs).toEqual([
  155 |       '/states/ny/', '/voters/?f_state=NY',
  156 |       expect.stringMatching(/^\/places\/[0-9a-f-]+\/$/), '/voters/?f_state=NY&f_city=ROCHESTER',
  157 |       expect.stringMatching(/^\/postalcodes\/[0-9a-f-]+\/$/), '/voters/?f_state=NY&f_zip=14620',
  158 |       expect.stringMatching(/^\/counties\/[0-9a-f-]+\/$/), '/voters/?f_state=NY&f_county=MONROE',
  159 |     ]);
  160 |     await expect(location.locator('a[href^="/counties/"]')).toHaveText('Monroe County');
  161 |     await expect(page.locator('section#overview a')).toHaveCount(0);
  162 | 
  163 |     await districts.locator('a', { hasText: 'NY-055' }).click();
  164 |     await expect(page).toHaveURL(/\/voters\/\?f_state=NY&f_senate=NY-055$/);
  165 |     await expect(page.locator('.entity-search-result').first()).toContainText(', NY', { timeout: DATA_LOAD_TIMEOUT });
  166 |     await expect(page.locator('#entity-filter-senate')).toHaveValue('NY-055', { timeout: DATA_LOAD_TIMEOUT });
  167 |   });
  168 | 
  169 |   test('an unknown voter id renders Not Found', async ({ page }) => {
  170 |     await page.goto('/voters/00000000-0000-0000-0000-000000000000/');
  171 |     await expect(page.locator('.leaf-detail-page h1')).toHaveText(/Voter Not Found/, { timeout: DATA_LOAD_TIMEOUT });
  172 |   });
  173 | });
  174 | 
  175 | test.describe('voter Location card (mocked)', () => {
  176 |   const voter = {
  177 |     id: 'v-loc', name_last: 'MOLLICA', name_first: 'CAROL', state: 'MA', addr_state: 'MA',
  178 |     city: 'WRENTHAM', zip: '02093', county: 'NORFOLK', congressional_district: 'MA-04',
  179 |     state_senate_district: 'MA-D14', state_house_district: 'MA-104', precinct: '25021823150003',
  180 |   };
  181 | 
  182 |   test('State, Place, Postal code and County link their pages plus the filtered voters list; Overview and Districts drop them', async ({ page }) => {
  183 |     const lookups: string[] = [];
  184 |     await page.route('**/api/graphql', async route => {
  185 |       const body = route.request().postData() || '';
  186 |       if (route.request().url().includes('/voters/api/graphql')) {
  187 |         return route.fulfill({ json: { data: { voter } } });
  188 |       }
  189 |       lookups.push(body);
  190 |       return route.fulfill({ json: { data: {
  191 |         postalCodes: [{ id: 'zip-02093', zip_code: '02093', overlapsCounties: [
  192 |           { id: 'cty-bristol', name: 'Bristol County', state: 'MA' },
  193 |           { id: 'cty-norfolk', name: 'Norfolk County', state: 'MA' },
```