# 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:93:7

# Error details

```
Error: apiRequestContext.post: connect ECONNREFUSED 10.1.105.22:80
Call log:
  - → POST http://10.1.105.22/api/graphql
    - user-agent: Playwright/1.64.1 (x64; ubuntu 22.04) node/20.9 CI/1
    - accept: */*
    - accept-encoding: gzip,deflate,br
    - content-type: application/json
    - content-length: 68

```

# Test source

```ts
  1   | import { test, expect, type APIRequestContext, type APIResponse } from '@playwright/test';
  2   | import { mintVisitorToken, skipIfPremiumGated, 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', {
      |                             ^ Error: apiRequestContext.post: connect ECONNREFUSED 10.1.105.22:80
  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  |     await skipIfPremiumGated(request, '/voters/');
  65  |     const res = await request.post('/voters/api/graphql', { data: { query: BROWSE } });
  66  |     expect(res.status()).toBe(200);
  67  |     expect(res.headers()['cache-control']).toContain('no-store');
  68  |     const body = await res.json() as { data?: { browseEntities?: { results: Array<{ id: string; name: string }> } } };
  69  |     expect(body.data?.browseEntities?.results.length).toBe(1);
  70  | 
  71  |     const get = await request.get('/voters/api/graphql', { maxRedirects: 0 });
  72  |     expect(get.status()).toBe(405);
  73  |   });
  74  | 
  75  |   test('browse lists the most active voters and links to detail pages', async ({ page, request }) => {
  76  |     await skipIfPremiumGated(request, '/voters/');
  77  |     const response = await page.goto('/voters/');
  78  |     expect(response?.status()).toBe(200);
  79  |     const results = page.locator('.entity-search-result');
  80  |     await expect(results.first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  81  |     await expect(results.first()).toContainText(/elections voted/);
  82  | 
  83  |     const href = await results.first().getAttribute('href');
  84  |     expect(href).toMatch(/^\/voters\/[0-9a-f-]{36}\/$/);
  85  |     const detail = await page.goto(href!);
  86  |     expect(detail?.status()).toBe(200);
  87  |     await expect(page.locator('.leaf-detail-page h1')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  88  |     await expect(page.locator('.leaf-detail-page h1')).not.toHaveText(/Not Found/i);
  89  |     await expect(page.locator('section#history')).toContainText('Elections voted');
  90  |     await expect(page.locator('meta[name="robots"]')).toHaveAttribute('content', 'noindex, follow');
  91  |   });
  92  | 
  93  |   test('search needs every term: name plus city', async ({ page, request }) => {
  94  |     await skipIfPremiumGated(request, '/voters/');
  95  |     await page.goto('/voters/');
  96  |     await page.fill('#entity-search-input', 'john smith austin');
  97  |     await page.locator('#entity-search-form button[type="submit"]').click();
  98  |     const results = page.locator('.entity-search-result');
  99  |     await expect(results.first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  100 |     await expect(results.filter({ hasText: /SMITH, JOHN/ }).first()).toContainText(/Austin, TX/i);
  101 |   });
  102 | 
  103 |   test('state filter narrows the list and scopes the county and district dropdowns', async ({ page, request }) => {
  104 |     await skipIfPremiumGated(request, '/voters/');
  105 |     await page.goto('/voters/');
  106 |     await expect(page.locator('.entity-search-result').first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  107 |     await expect(page.locator('.entity-filter-control[data-key="county"]')).toBeHidden();
  108 |     await page.selectOption('#entity-filter-state', 'FL');
  109 |     await expect(page.locator('.entity-search-result').first()).toContainText(', FL', { timeout: DATA_LOAD_TIMEOUT });
  110 |     await expect(page).toHaveURL(/[?&]f_state=FL/);
  111 |     await expect(page.locator('#entity-filter-county option', { hasText: /Miami-Dade \([\d,]+\)/ })).toHaveCount(1, { timeout: DATA_LOAD_TIMEOUT });
  112 |     await expect(page.locator('#entity-filter-cd option', { hasText: 'FL-27' })).toHaveCount(1);
  113 |   });
  114 | 
  115 |   test('filters combine, count matches and survive a reload', async ({ page, request }) => {
  116 |     await skipIfPremiumGated(request, '/voters/');
```