# 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/follow-control.spec.ts >> profile Follow (sites#435) >> signed in: Follow toggles to Following ✓ and back through account-api
- Location: tests/ui/follow-control.spec.ts:69:7

# Error details

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

```
Error: apiRequestContext.get: Request context disposed.
Call log:
  - → GET http://10.1.105.22/candidates/daa9a544-52e2-83cd-03fd-befc14a52af4/
    - user-agent: Playwright/1.64.1 (x64; ubuntu 22.04) node/20.9 CI/1
    - accept: */*
    - accept-encoding: gzip,deflate,br

```

# Test source

```ts
  1   | import { test, expect, Page, APIRequestContext } from '@playwright/test';
  2   | 
  3   | // Follow on us.elect.info profile pages (sites#435, epic #430). us-pages puts
  4   | // the page entity on <body data-entity-id/-type/-name>; auth.js probes the
  5   | // news index through /api/graphql (visitor token) and, when there is news,
  6   | // shows Follow / Following ✓ and "News on elect.news ↗".
  7   | //
  8   | // Identity and account-api are mocked in the first group, so it runs on CI
  9   | // pods and preview alike; the news probe is real (the API behind the host).
  10  | // The last group follows and unfollows for real through account-api, and runs
  11  | // only where the visitor is really signed in (preview.elect.info front door as
  12  | // the preview-e2e service account: PLAYWRIGHT_AUTH_USER/PASS).
  13  | 
  14  | const TALARICO = 'daa9a544-52e2-83cd-03fd-befc14a52af4'; // S6TX00479, in the news
  15  | const DEM = '12cac40d-b40d-abcc-6735-5e49f3925192'; // Democratic Party, in the news
  16  | const DNC = 'a75a9110-f1db-a945-39a8-7503ed3c04da'; // C00010603: no matched articles
  17  | const CANDIDATE = `/candidates/${TALARICO}/`;
  18  | const PARTY = `/parties/${DEM}/`;
  19  | const COMMITTEE = `/committees/${DNC}/`;
  20  | 
  21  | /** Skip when the host's us-pages predates the page entity on <body>. */
  22  | async function needsEntity(request: APIRequestContext, path: string): Promise<void> {
> 23  |   const html = await (await request.get(path)).text();
      |                                     ^ Error: apiRequestContext.get: Request context disposed.
  24  |   test.skip(!/<body[^>]*data-entity-id=/.test(html), 'the us-pages renderer predates data-entity-id (sites#435)');
  25  | }
  26  | 
  27  | /**
  28  |  * Mock identity (Authentik on) and the visitor's follows. Returns the account-api calls made, and `signIn` to switch
  29  |  * the mocked visitor (the next page load sees it).
  30  |  */
  31  | async function as(page: Page, signedInAs: string, follows: string[] = []) {
  32  |   const calls: { method: string; url: string; body: unknown }[] = [];
  33  |   const mine = new Set(follows);
  34  |   let email = signedInAs;
  35  |   await page.route('**/auth/whoami*', r => r.fulfill({ json: { enabled: 'true', email, elevated: false, entitlements: {} } }));
  36  |   await page.route('**/auth/account/me*', r => r.fulfill({ json: { email, requests: [], agreement: null, granted: [] } }));
  37  |   await page.route('**/auth/account/follows**', async r => {
  38  |     const req = r.request();
  39  |     const url = new URL(req.url());
  40  |     const body = req.postData() ? JSON.parse(req.postData()!) : null;
  41  |     calls.push({ method: req.method(), url: url.pathname, body });
  42  |     if (!email) return r.fulfill({ status: 401, json: { error: 'Sign in first.' } });
  43  |     if (req.method() === 'GET') {
  44  |       return r.fulfill({ json: { email, max: 200, follows: [...mine].map(entityId => ({ entityId, entityType: 'Candidate', entityName: 'X' })) } });
  45  |     }
  46  |     expect(req.headers()['x-requested-with']).toBe('elect.info');
  47  |     if (req.method() === 'POST') {
  48  |       mine.add(body.entityId);
  49  |       return r.fulfill({ status: 201, json: { follow: body } });
  50  |     }
  51  |     const id = decodeURIComponent(url.pathname.split('/').pop() || '');
  52  |     mine.delete(id);
  53  |     return r.fulfill({ json: { ok: true } });
  54  |   });
  55  |   return Object.assign(calls, { signIn: (to: string) => { email = to; } });
  56  | }
  57  | 
  58  | test.describe('profile Follow (sites#435)', () => {
  59  |   test('profile pages carry their entity on <body>', async ({ request }) => {
  60  |     await needsEntity(request, CANDIDATE);
  61  |     for (const [path, id, type] of [[CANDIDATE, TALARICO, 'Candidate'], [PARTY, DEM, 'Party'], [COMMITTEE, DNC, 'Committee']]) {
  62  |       const html = await (await request.get(path)).text();
  63  |       expect(html, path).toContain(`data-entity-id="${id}"`);
  64  |       expect(html, path).toContain(`data-entity-type="${type}"`);
  65  |       expect(html, path).toMatch(/data-entity-name="[^"]+"/);
  66  |     }
  67  |   });
  68  | 
  69  |   test('signed in: Follow toggles to Following ✓ and back through account-api', async ({ page, request }) => {
  70  |     await needsEntity(request, CANDIDATE);
  71  |     const calls = await as(page, 'jane@example.org');
  72  |     await page.goto(CANDIDATE);
  73  |     const button = page.locator('#ei-follow .ei-follow__btn');
  74  |     await expect(button).toHaveText('Follow', { timeout: 20000 });
  75  |     await expect(button).toHaveAttribute('aria-pressed', 'false');
  76  |     // In the page header, under the name.
  77  |     await expect(page.locator('body > header #ei-follow')).toHaveCount(1);
  78  | 
  79  |     await button.click();
  80  |     await expect(button).toHaveText('Following ✓');
  81  |     await expect(button).toHaveAttribute('aria-pressed', 'true');
  82  |     const post = calls.find(c => c.method === 'POST');
  83  |     expect(post?.url).toBe('/auth/account/follows');
  84  |     expect(post?.body).toMatchObject({ entityId: TALARICO, entityType: 'Candidate' });
  85  |     expect((post?.body as { entityName: string }).entityName).toBe(await page.locator('header h1').innerText());
  86  | 
  87  |     await button.click();
  88  |     await expect(button).toHaveText('Follow');
  89  |     expect(calls.find(c => c.method === 'DELETE')?.url).toBe(`/auth/account/follows/${TALARICO}`);
  90  |   });
  91  | 
  92  |   test('signed in and already following: Following ✓', async ({ page, request }) => {
  93  |     await needsEntity(request, CANDIDATE);
  94  |     await as(page, 'jane@example.org', [TALARICO]);
  95  |     await page.goto(CANDIDATE);
  96  |     await expect(page.locator('#ei-follow .ei-follow__btn')).toHaveText('Following ✓', { timeout: 20000 });
  97  |   });
  98  | 
  99  |   test('News on elect.news opens the filtered feed in a new tab', async ({ page, request }) => {
  100 |     await needsEntity(request, PARTY);
  101 |     await as(page, '');
  102 |     await page.goto(PARTY);
  103 |     const link = page.locator('#ei-follow a.ei-follow__news');
  104 |     await expect(link).toHaveText('News on elect.news ↗', { timeout: 20000 });
  105 |     await expect(link).toHaveAttribute('href', `https://us.elect.news/?entity=${DEM}&scope=all`);
  106 |     await expect(link).toHaveAttribute('target', '_blank');
  107 |     await expect(link).toHaveAttribute('rel', /noopener/);
  108 |   });
  109 | 
  110 |   test('signed out: Follow opens the sign-in panel, and the follow completes after sign-in', async ({ page, request }) => {
  111 |     await needsEntity(request, CANDIDATE);
  112 |     const calls = await as(page, '');
  113 |     await page.goto(CANDIDATE);
  114 |     const button = page.locator('#ei-follow .ei-follow__btn');
  115 |     await expect(button).toHaveText('Follow', { timeout: 20000 });
  116 |     await button.click();
  117 |     await expect(page.locator('dialog#ei-signin h2')).toContainText('Sign in to follow');
  118 |     expect(calls.filter(c => c.method !== 'GET')).toEqual([]);
  119 | 
  120 |     // Back signed in (same tab, so the same sessionStorage): the follow is made without another click.
  121 |     calls.signIn('jane@example.org');
  122 |     await page.reload();
  123 |     await expect(button).toHaveText('Following ✓', { timeout: 20000 });
```