# 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/visitor-token.spec.ts >> donor records are premium >> an anonymous visitor sees employer totals but not donor names
- Location: tests/ui/visitor-token.spec.ts:109:7

# Error details

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

Locator: locator('#donors .premium-required')
Expected: visible
Timeout: 5000ms
Error: element(s) not found

Call log:
  - Expect "toBeVisible" locator('#donors .premium-required') with timeout 5000ms
  - waiting for locator('#donors .premium-required')

```

```yaml
- banner:
  - heading "SPACE EXPLORATION TECHNOLOGIES CORP." [level=1]
  - paragraph: Employer Organization
- text: Not signed in
- link "Log in":
  - /url: /outpost.goauthentik.io/start?rd=https%3A%2F%2Fus.elect.info%2Femployers%2F5f9a1727-2225-3699-685b-8fb15c17c8f3%2F
- link "Create an account":
  - /url: /signin/register?rd=https%3A%2F%2Fus.elect.info%2Femployers%2F5f9a1727-2225-3699-685b-8fb15c17c8f3%2F
- toolbar "Member tools":
  - button "All cycles (Sign in to use the cycle facet)" [disabled]: All cycles
  - button "Graph (Sign in to use Graph)" [disabled]: Graph
- heading "Overview" [level=2]
- term: Name
- definition: SPACE EXPLORATION TECHNOLOGIES CORP.
- term: Normalized name
- definition: SPACE EXPLORATION TECHNOLOGIES CORP.
- term: Cycles
- definition: 2024,2014,2020,2016,2026,2012,2018,2022,2008
- term: First contribution
- definition: Nov 3, 2008
- term: Last contribution
- definition: Aug 28, 2026
- heading "Employee Contribution Activity" [level=2]
- term: Total contributed
- definition: $326,411,709.00
- term: Employee donors
- definition: 6,050
- term: Contributions
- definition: 59,713
- term: Committees
- definition: "22"
- heading "Top Employee Donors (up to 20)" [level=2]
- table:
  - rowgroup:
    - row "Individual Occupation Donor total Records":
      - columnheader "Individual"
      - columnheader "Occupation"
      - columnheader "Donor total"
      - columnheader "Records"
  - rowgroup:
    - row "MUSK, ELON CEO $247,762,036.00 49":
      - cell "MUSK, ELON":
        - link "MUSK, ELON":
          - /url: /individuals/b15bd39d-9bb6-b25e-a823-28c44e15b15b/
      - cell "CEO"
      - cell "$247,762,036.00"
      - cell "49"
    - row "JOHNSEN, BRET CHIEF FINANCIAL OFFICER $30,896.00 78":
      - cell "JOHNSEN, BRET":
        - link "JOHNSEN, BRET":
          - /url: /individuals/55c2cfd8-8e1f-b367-66b2-19fa4b338a2c/
      - cell "CHIEF FINANCIAL OFFICER"
      - cell "$30,896.00"
      - cell "78"
    - row "BJELDE, BRIAN N. VP, HUMAN RESOURCES $30,528.00 77":
      - cell "BJELDE, BRIAN N.":
        - link "BJELDE, BRIAN N.":
          - /url: /individuals/29940a9e-d500-22b1-ed0f-e34921af62b1/
      - cell "VP, HUMAN RESOURCES"
      - cell "$30,528.00"
      - cell "77"
    - row "JUNCOSA, MARK VP, STARSHIP $30,528.00 77":
      - cell "JUNCOSA, MARK":
        - link "JUNCOSA, MARK":
          - /url: /individuals/0ada5650-896b-c7a3-36ed-c8fe6057b2fa/
      - cell "VP, STARSHIP"
      - cell "$30,528.00"
      - cell "77"
    - row "SHOTWELL, GWYNNE PRESIDENT AND COO $30,528.00 77":
      - cell "SHOTWELL, GWYNNE":
        - link "SHOTWELL, GWYNNE":
          - /url: /individuals/1cf0a750-54a1-7f57-f845-ae476d3f49b6/
      - cell "PRESIDENT AND COO"
      - cell "$30,528.00"
      - cell "77"
    - row "HUGHES, TIMOTHY SVP, GLOBAL BUSINESS AND GOV $28,608.00 73":
      - cell "HUGHES, TIMOTHY":
        - link "HUGHES, TIMOTHY":
          - /url: /individuals/09817ec1-3256-e327-d9d2-5d5459e31604/
      - cell "SVP, GLOBAL BUSINESS AND GOV"
      - cell "$28,608.00"
      - cell "73"
    - row "HOFELLER, JONATHAN VP, STARLINK COMMERCIAL SALES $21,733.00 57":
      - cell "HOFELLER, JONATHAN":
        - link "HOFELLER, JONATHAN":
          - /url: /individuals/febcf97e-3e08-7732-ad23-9dbeac5a9ce3/
      - cell "VP, STARLINK COMMERCIAL SALES"
      - cell "$21,733.00"
      - cell "57"
    - row "SADHWANI, ANAND H. PRINCIPAL PROPULSION ANALYST $18,177.00 6":
      - cell "SADHWANI, ANAND H.":
        - link "SADHWANI, ANAND H.":
          - /url: /individuals/b8be1e68-7a39-78d2-c23c-391956a93454/
      - cell "PRINCIPAL PROPULSION ANALYST"
      - cell "$18,177.00"
      - cell "6"
    - row "URSENBACH, JOSHUA MANAGER, IT SYSTEMS ENGINEERING $13,430.00 43":
      - cell "URSENBACH, JOSHUA":
        - link "URSENBACH, JOSHUA":
          - /url: /individuals/34928337-8dfc-495e-59dd-ec0d166c6272/
      - cell "MANAGER, IT SYSTEMS ENGINEERING"
      - cell "$13,430.00"
      - cell "43"
    - row "O'SHAUGHNESSY, TERRENCE SR. ADVISOR AND VP, SPECIAL $13,300.00 64":
      - cell "O'SHAUGHNESSY, TERRENCE":
        - link "O'SHAUGHNESSY, TERRENCE":
          - /url: /individuals/c7af010c-5627-2136-646c-82c81a47c4f1/
      - cell "SR. ADVISOR AND VP, SPECIAL"
      - cell "$13,300.00"
      - cell "64"
    - row "HEWITT, STEVEN SUPPLIER QUALITY ENGINEER $12,900.00 112":
      - cell "HEWITT, STEVEN":
        - link "HEWITT, STEVEN":
          - /url: /individuals/eaf76f90-4e1d-b9e4-5a74-fe17bd8d0dd6/
      - cell "SUPPLIER QUALITY ENGINEER"
      - cell "$12,900.00"
      - cell "112"
    - row "ROSSONI, MICHAEL PRINCIPAL PROPULSION ENGINEER $12,313.00 51":
      - cell "ROSSONI, MICHAEL":
        - link "ROSSONI, MICHAEL":
          - /url: /individuals/06d79585-15ce-1244-e139-c4014c7176e4/
      - cell "PRINCIPAL PROPULSION ENGINEER"
      - cell "$12,313.00"
      - cell "51"
    - row "DREYER, LAUREN SR. DIRECTOR, STARLINK BUSINES $11,930.00 43":
      - cell "DREYER, LAUREN":
        - link "DREYER, LAUREN":
          - /url: /individuals/70438318-57e5-c203-16cd-7fb715099c87/
      - cell "SR. DIRECTOR, STARLINK BUSINES"
      - cell "$11,930.00"
      - cell "43"
    - row "DUNN, MATHEW P. SR. DIRECTOR, GOVERNMENT AFFAI $11,872.00 45":
      - cell "DUNN, MATHEW P.":
        - link "DUNN, MATHEW P.":
          - /url: /individuals/8da7f0ff-be7a-47f8-2ea8-e19234ea2702/
      - cell "SR. DIRECTOR, GOVERNMENT AFFAI"
      - cell "$11,872.00"
      - cell "45"
    - row "PEDEN, ROBERT DIRECTOR, TEXAS TEST OPERATIONS $11,250.00 4":
      - cell "PEDEN, ROBERT":
        - link "PEDEN, ROBERT":
          - /url: /individuals/b55c9404-5955-782c-b171-696f574a6862/
      - cell "DIRECTOR, TEXAS TEST OPERATIONS"
      - cell "$11,250.00"
      - cell "4"
    - row "HINZO, ROBERT PRODUCT SUPERVISOR $10,700.00 94":
      - cell "HINZO, ROBERT":
        - link "HINZO, ROBERT":
          - /url: /individuals/fd5f53b4-6242-d5df-16b2-73062e2c045c/
      - cell "PRODUCT SUPERVISOR"
      - cell "$10,700.00"
      - cell "94"
    - row "COOPER, PATRICIA A. VICE PRESIDENT OF SATELLITE OF GOVERNM $9,400.00 46":
      - cell "COOPER, PATRICIA A.":
        - link "COOPER, PATRICIA A.":
          - /url: /individuals/bc7c9c98-01cd-1281-eaa8-e92b67bc6c8f/
      - cell "VICE PRESIDENT OF SATELLITE OF GOVERNM"
      - cell "$9,400.00"
      - cell "46"
    - row "SCHENEWERK, CARYN ATTORNEY $9,000.00 10":
      - cell "SCHENEWERK, CARYN":
        - link "SCHENEWERK, CARYN":
          - /url: /individuals/e349fe00-e9e5-08ae-6fdc-6fd47642a642/
      - cell "ATTORNEY"
      - cell "$9,000.00"
      - cell "10"
    - row "CUMMINGS, NICHOLAS SR. DIRECTOR, PROGRAM DEVELOPM $8,705.00 55":
      - cell "CUMMINGS, NICHOLAS":
        - link "CUMMINGS, NICHOLAS":
          - /url: /individuals/3c4ffaf6-c75c-b381-7429-bcc3068b9ff9/
      - cell "SR. DIRECTOR, PROGRAM DEVELOPM"
      - cell "$8,705.00"
      - cell "55"
    - row "JENSEN, JESSICA VP, CUSTOMER OPERATIONS AND $8,165.00 35":
      - cell "JENSEN, JESSICA":
        - link "JENSEN, JESSICA":
          - /url: /individuals/b88c3512-b040-cfcc-1c40-422c310be10c/
      - cell "VP, CUSTOMER OPERATIONS AND"
      - cell "$8,165.00"
      - cell "35"
- contentinfo:
  - paragraph:
    - link "elect.info":
      - /url: https://us.elect.info/
    - text: "|"
    - link "Browse Employers":
      - /url: /employers/
```

# Test source

```ts
  12  | 
  13  | /** A request context with an empty cookie jar, so a token from one probe does not leak into the next. */
  14  | async function freshContext(baseURL: string | undefined): Promise<APIRequestContext> {
  15  |   return playwrightRequest.newContext({ baseURL });
  16  | }
  17  | 
  18  | async function hasGate(baseURL: string | undefined): Promise<boolean> {
  19  |   const probe = await freshContext(baseURL);
  20  |   try {
  21  |     return (await probe.get('/api/token')).status() === 204;
  22  |   } finally {
  23  |     await probe.dispose();
  24  |   }
  25  | }
  26  | 
  27  | // js/auth.js requests /api/token before every /api/graphql call that has no
  28  | // token, on every page that loads data. It must never be an error: 204 where the
  29  | // gate runs (preview and production since the prod cutover), 200 from the old stub.
  30  | test('GET /api/token is never an error, with or without the gate', async ({ baseURL }) => {
  31  |   const probe = await freshContext(baseURL);
  32  |   try {
  33  |     const res = await probe.get('/api/token');
  34  |     expect([200, 204]).toContain(res.status());
  35  |     expect(res.headers()['cache-control']).toContain('no-store');
  36  |   } finally {
  37  |     await probe.dispose();
  38  |   }
  39  | });
  40  | 
  41  | test.describe('visitor-token gate', () => {
  42  |   test('GET /api/token sets an HttpOnly token and a readable expiry', async ({ request }) => {
  43  |     const res = await request.get('/api/token');
  44  |     // Without the gate the path falls through to the site (404 or a page); a 5xx is a broken gate, not a skip.
  45  |     test.skip(res.status() !== 204 && res.status() < 500, 'this target has no visitor-token gate');
  46  |     expect(res.status()).toBe(204);
  47  |     expect(res.headers()['cache-control']).toContain('no-store');
  48  |     const cookies = res.headersArray().filter(h => h.name.toLowerCase() === 'set-cookie').map(h => h.value);
  49  |     const token = cookies.find(c => c.startsWith('ei_vt='));
  50  |     const exp = cookies.find(c => c.startsWith('ei_vt_exp='));
  51  |     expect(token).toMatch(/^ei_vt=[\w-]+\.[\w-]+\.[\w-]+; Path=\/api\/; Max-Age=\d+; HttpOnly; SameSite=Strict/);
  52  |     // <exp>.<tier>, then the entitlement key since the identity bar (sites#305): one t/f per restricted section.
  53  |     expect(exp).toMatch(/^ei_vt_exp=\d+\.(anon|member|premium)(\.[tf]+)?; Path=\/;/);
  54  |     expect(exp).not.toContain('HttpOnly');
  55  |   });
  56  | 
  57  |   test('without a token, /api/graphql answers 401', async ({ request, baseURL }) => {
  58  |     test.skip(!(await hasGate(baseURL)), 'this target has no visitor-token gate');
  59  |     const res = await request.post('/api/graphql', { data: { query: LISTED, variables: { state: 'VT' } } });
  60  |     expect(res.status()).toBe(401);
  61  | 
  62  |     const forged = await request.post('/api/graphql', {
  63  |       data: { query: LISTED, variables: { state: 'VT' } },
  64  |       headers: { Cookie: 'ei_vt=eyJhbGciOiJIUzI1NiJ9.eyJ0aWVyIjoicHJlbWl1bSJ9.AAAA' },
  65  |     });
  66  |     expect(forged.status()).toBe(401);
  67  |   });
  68  | 
  69  |   test('an anon token runs persisted documents and nothing else', async ({ request, baseURL }) => {
  70  |     test.skip(!(await hasGate(baseURL)), 'this target has no visitor-token gate');
  71  |     // The token goes into this context's cookie jar (Path=/api/), as in a browser.
  72  |     expect((await request.get('/api/token')).status()).toBe(204);
  73  | 
  74  |     const listed = await request.post('/api/graphql', { data: { query: LISTED, variables: { state: 'VT' } } });
  75  |     expect(listed.status()).toBe(200);
  76  |     const body = await listed.json() as { data?: { counties?: Array<{ state: string }> } };
  77  |     expect(body.data?.counties?.length).toBeGreaterThan(0);
  78  | 
  79  |     // Whitespace does not matter: the gate hashes the collapsed document.
  80  |     const reflowed = await request.post('/api/graphql', {
  81  |       data: { query: LISTED.replace(/ \{ /g, ' {\n    '), variables: { state: 'VT' } },
  82  |     });
  83  |     expect(reflowed.status()).toBe(200);
  84  | 
  85  |     const unlisted = await request.post('/api/graphql', { data: { query: UNLISTED } });
  86  |     expect(unlisted.status()).toBe(403);
  87  |     expect(await unlisted.json()).toEqual({ error: 'query not allowed' });
  88  | 
  89  |     const batch = await request.post('/api/graphql', {
  90  |       data: [{ query: LISTED, variables: { state: 'VT' } }, { query: UNLISTED }],
  91  |     });
  92  |     expect(batch.status(), 'a batch is refused when any document is unlisted').toBe(403);
  93  |   });
  94  | });
  95  | 
  96  | // Donor records are premium (sites#277): an anonymous visitor sees the premium
  97  | // notice instead of donors; the employer's totals stay public.
  98  | test.describe('donor records are premium', () => {
  99  |   test.beforeEach(async ({ baseURL }) => {
  100 |     test.skip(!(await hasGate(baseURL)), 'this target has no visitor-token gate');
  101 |   });
  102 | 
  103 |   test('an anonymous visitor gets the premium notice on Individual Donors', async ({ page }) => {
  104 |     await page.goto('/individuals/');
  105 |     await expect(page.locator('.premium-required')).toBeVisible({ timeout: 30_000 });
  106 |     await expect(page.locator('.entity-search-result')).toHaveCount(0);
  107 |   });
  108 | 
  109 |   test('an anonymous visitor sees employer totals but not donor names', async ({ page }) => {
  110 |     await page.goto('/employers/5f9a1727-2225-3699-685b-8fb15c17c8f3/');
  111 |     await expect(page.locator('#activity')).toBeVisible({ timeout: 30_000 });
> 112 |     await expect(page.locator('#donors .premium-required')).toBeVisible();
      |                                                             ^ Error: expect(locator).toBeVisible() failed
  113 |     await expect(page.locator('#donors a[href^="/individuals/"]')).toHaveCount(0);
  114 |   });
  115 | });
  116 | 
```