# 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/entity-filters.spec.ts >> individual donor filters >> text filters: employer and ZIP prefix
- Location: tests/ui/entity-filters.spec.ts:50:7

# Error details

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

```
Error: expect(locator).toBeVisible() failed

Locator: locator('.entity-search-result').first()
Expected: visible
Error: element(s) not found

Call log:
  - Expect "toBeVisible" locator('.entity-search-result').first() with timeout 30000ms
  - waiting for locator('.entity-search-result').first()
  - Test timeout of 30000ms exceeded.

```

```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.eyJpc3MiOiJnb2F1dGhlbnRpay5pby9vdXRwb3N0L0tJNUJlOHNjTFZlWk43bjV6WThwNXM4VmJweXJhZE9TS1F2bzlUMmMiLCJzaWQiOiIzVjZWVk42NkRZUkIyUlA1RjVFU0o0VjdDSFNTM0w1RUk0TVhIWVA3NTYyNEVGQVdYVVRBIiwic3RhdGUiOiJyUW45R01DR1kzQXJoc3Npa1AtbDVHdXhLM292d1Y0MEVtOGFVaU50ZFBrIiwicmVkaXJlY3QiOiJodHRwczovL3VzLmVsZWN0LmluZm8vaW5kaXZpZHVhbHMvP2ZfZW1wbG95ZXI9Z29vZ2xlXHUwMDI2Zl96aXA9OTQwIn0.D34XWtO_FK9rx4eExhZTwHhxOM03Ih1txqgbSFoUC30&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.eyJpc3MiOiJnb2F1dGhlbnRpay5pby9vdXRwb3N0L0tJNUJlOHNjTFZlWk43bjV6WThwNXM4VmJweXJhZE9TS1F2bzlUMmMiLCJzaWQiOiIzVjZWVk42NkRZUkIyUlA1RjVFU0o0VjdDSFNTM0w1RUk0TVhIWVA3NTYyNEVGQVdYVVRBIiwic3RhdGUiOiJyUW45R01DR1kzQXJoc3Npa1AtbDVHdXhLM292d1Y0MEVtOGFVaU50ZFBrIiwicmVkaXJlY3QiOiJodHRwczovL3VzLmVsZWN0LmluZm8vaW5kaXZpZHVhbHMvP2ZfZW1wbG95ZXI9Z29vZ2xlXHUwMDI2Zl96aXA9OTQwIn0.D34XWtO_FK9rx4eExhZTwHhxOM03Ih1txqgbSFoUC30
    - 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.eyJpc3MiOiJnb2F1dGhlbnRpay5pby9vdXRwb3N0L0tJNUJlOHNjTFZlWk43bjV6WThwNXM4VmJweXJhZE9TS1F2bzlUMmMiLCJzaWQiOiIzVjZWVk42NkRZUkIyUlA1RjVFU0o0VjdDSFNTM0w1RUk0TVhIWVA3NTYyNEVGQVdYVVRBIiwic3RhdGUiOiJyUW45R01DR1kzQXJoc3Npa1AtbDVHdXhLM292d1Y0MEVtOGFVaU50ZFBrIiwicmVkaXJlY3QiOiJodHRwczovL3VzLmVsZWN0LmluZm8vaW5kaXZpZHVhbHMvP2ZfZW1wbG95ZXI9Z29vZ2xlXHUwMDI2Zl96aXA9OTQwIn0.D34XWtO_FK9rx4eExhZTwHhxOM03Ih1txqgbSFoUC30&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.eyJpc3MiOiJnb2F1dGhlbnRpay5pby9vdXRwb3N0L0tJNUJlOHNjTFZlWk43bjV6WThwNXM4VmJweXJhZE9TS1F2bzlUMmMiLCJzaWQiOiIzVjZWVk42NkRZUkIyUlA1RjVFU0o0VjdDSFNTM0w1RUk0TVhIWVA3NTYyNEVGQVdYVVRBIiwic3RhdGUiOiJyUW45R01DR1kzQXJoc3Npa1AtbDVHdXhLM292d1Y0MEVtOGFVaU50ZFBrIiwicmVkaXJlY3QiOiJodHRwczovL3VzLmVsZWN0LmluZm8vaW5kaXZpZHVhbHMvP2ZfZW1wbG95ZXI9Z29vZ2xlXHUwMDI2Zl96aXA9OTQwIn0.D34XWtO_FK9rx4eExhZTwHhxOM03Ih1txqgbSFoUC30
```

# Test source

```ts
  1  | import { test, expect, type APIRequestContext } from '@playwright/test';
  2  | import { visitorHeaders, premiumVisitor } from './helpers';
  3  | 
  4  | // Structured filters on the Individual Donors page (electinfo/sites#277).
  5  | // Voter filters are covered in voters.spec.ts.
  6  | 
  7  | const DATA_LOAD_TIMEOUT = 30_000;
  8  | 
  9  | /** The CI test pod proxies to the production API, which gains filters only with the release. */
  10 | async function apiHasFilters(request: APIRequestContext): Promise<boolean> {
  11 |   const res = await request.post('/api/graphql', {
  12 |     data: { query: '{ __type(name: "EntityFilterInput") { name } }' },
  13 |     headers: visitorHeaders(),
  14 |   });
  15 |   if (res.status() !== 200) return false;
  16 |   const body = await res.json() as { data?: { __type?: { name?: string } | null } };
  17 |   return body.data?.__type?.name === 'EntityFilterInput';
  18 | }
  19 | 
  20 | test.describe('individual donor filters', () => {
  21 |   test.beforeEach(async ({ request }) => {
  22 |     test.skip(!(await apiHasFilters(request)), 'the API behind this host has no structured filters yet');
  23 |   });
  24 |   // Donor records are premium (sites#277).
  25 |   test.beforeEach(async ({ context, baseURL }) => {
  26 |     await premiumVisitor(context, baseURL);
  27 |   });
  28 | 
  29 |   test('state, cycle, lifetime total and lean narrow the list and land in the URL', async ({ page }) => {
  30 |     await page.goto('/individuals/');
  31 |     const results = page.locator('.entity-search-result');
  32 |     await expect(results.first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  33 | 
  34 |     await page.selectOption('#entity-filter-state', 'TX');
  35 |     await expect(results.first()).toContainText(', TX', { timeout: DATA_LOAD_TIMEOUT });
  36 |     await page.selectOption('#entity-filter-total', '10000');
  37 |     await page.selectOption('#entity-filter-lean', 'strong-r');
  38 |     await page.locator('#entity-filter-cycle summary').click();
  39 |     await page.locator('#entity-filter-cycle input[value="2024"]').check();
  40 | 
  41 |     await expect(page).toHaveURL(/f_state=TX/);
  42 |     await expect(page).toHaveURL(/f_total=10000/);
  43 |     await expect(page).toHaveURL(/f_lean=strong-r/);
  44 |     await expect(page).toHaveURL(/f_cycle=2024/);
  45 |     await expect(page.locator('#entity-results-count')).toHaveText(/^[\d,]+ match(es)?$/, { timeout: DATA_LOAD_TIMEOUT });
  46 |     await expect(results.first()).toContainText(', TX');
  47 |     await expect(results.first()).toContainText(/REP: (8\d|9\d|100)%/);
  48 |   });
  49 | 
  50 |   test('text filters: employer and ZIP prefix', async ({ page }) => {
  51 |     await page.goto('/individuals/?f_employer=google&f_zip=940');
  52 |     const results = page.locator('.entity-search-result');
> 53 |     await expect(results.first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
     |                                   ^ Error: expect(locator).toBeVisible() failed
  54 |     await expect(results.first()).toContainText(/GOOGLE/i);
  55 |     await expect(page.locator('#entity-filter-employer')).toHaveValue('google');
  56 |   });
  57 | 
  58 |   test('an unknown filter field is refused by the API', async ({ request }) => {
  59 |     const res = await request.post('/api/graphql', {
  60 |       data: { query: '{ browseEntities(entityType: Individual, filters: [{ field: "name", values: ["x"] }]) { total } }' },
  61 |       headers: visitorHeaders(),
  62 |     });
  63 |     const body = await res.json() as { errors?: Array<{ message: string }> };
  64 |     expect(body.errors?.[0]?.message).toContain('Unknown filter field');
  65 |   });
  66 | });
  67 | 
```