# 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/entities.spec.ts >> entity browse sort order - quantitative fields >> individuals sorted by total contributions descending
- Location: tests/ui/entities.spec.ts:399:7

# Error details

```
TimeoutError: page.waitForResponse: Timeout 15000ms exceeded while waiting for event "response"
```

# 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.eyJpc3MiOiJnb2F1dGhlbnRpay5pby9vdXRwb3N0L0tJNUJlOHNjTFZlWk43bjV6WThwNXM4VmJweXJhZE9TS1F2bzlUMmMiLCJzaWQiOiJMM05BVE9PSTNWUU1UVFdZNVpTMzRKSDVaRERRSVBXVkNSQ0lGUjIyTEhNQkZESVU0TElBIiwic3RhdGUiOiJUQ0ZpNERmWWYyalp5SXF4ci1rWm8yUXlQWGI0RGR6Q0tBM1NqbGdmc1pVIiwicmVkaXJlY3QiOiJodHRwczovL3VzLmVsZWN0LmluZm8vaW5kaXZpZHVhbHMvIn0.r9AaPe0AFEEbNBiXTVvR-G9Rxax-UK5KYxlnyrwqEr4&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.eyJpc3MiOiJnb2F1dGhlbnRpay5pby9vdXRwb3N0L0tJNUJlOHNjTFZlWk43bjV6WThwNXM4VmJweXJhZE9TS1F2bzlUMmMiLCJzaWQiOiJMM05BVE9PSTNWUU1UVFdZNVpTMzRKSDVaRERRSVBXVkNSQ0lGUjIyTEhNQkZESVU0TElBIiwic3RhdGUiOiJUQ0ZpNERmWWYyalp5SXF4ci1rWm8yUXlQWGI0RGR6Q0tBM1NqbGdmc1pVIiwicmVkaXJlY3QiOiJodHRwczovL3VzLmVsZWN0LmluZm8vaW5kaXZpZHVhbHMvIn0.r9AaPe0AFEEbNBiXTVvR-G9Rxax-UK5KYxlnyrwqEr4
            - 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.eyJpc3MiOiJnb2F1dGhlbnRpay5pby9vdXRwb3N0L0tJNUJlOHNjTFZlWk43bjV6WThwNXM4VmJweXJhZE9TS1F2bzlUMmMiLCJzaWQiOiJMM05BVE9PSTNWUU1UVFdZNVpTMzRKSDVaRERRSVBXVkNSQ0lGUjIyTEhNQkZESVU0TElBIiwic3RhdGUiOiJUQ0ZpNERmWWYyalp5SXF4ci1rWm8yUXlQWGI0RGR6Q0tBM1NqbGdmc1pVIiwicmVkaXJlY3QiOiJodHRwczovL3VzLmVsZWN0LmluZm8vaW5kaXZpZHVhbHMvIn0.r9AaPe0AFEEbNBiXTVvR-G9Rxax-UK5KYxlnyrwqEr4&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.eyJpc3MiOiJnb2F1dGhlbnRpay5pby9vdXRwb3N0L0tJNUJlOHNjTFZlWk43bjV6WThwNXM4VmJweXJhZE9TS1F2bzlUMmMiLCJzaWQiOiJMM05BVE9PSTNWUU1UVFdZNVpTMzRKSDVaRERRSVBXVkNSQ0lGUjIyTEhNQkZESVU0TElBIiwic3RhdGUiOiJUQ0ZpNERmWWYyalp5SXF4ci1rWm8yUXlQWGI0RGR6Q0tBM1NqbGdmc1pVIiwicmVkaXJlY3QiOiJodHRwczovL3VzLmVsZWN0LmluZm8vaW5kaXZpZHVhbHMvIn0.r9AaPe0AFEEbNBiXTVvR-G9Rxax-UK5KYxlnyrwqEr4
```

# Test source

```ts
  260 |       if (!resp.url().includes('/api/graphql')) return false;
  261 |       if (resp.request().method() !== 'POST') return false;
  262 |       const body = resp.request().postData() || '';
  263 |       return body.includes('browseEntities');
  264 |     }, { timeout: DATA_LOAD_TIMEOUT });
  265 | 
  266 |     await page.goto(TEXAS_PATH);
  267 |     const response = await responsePromise;
  268 | 
  269 |     const requestBody = JSON.parse(response.request().postData() || '{}');
  270 |     expect(requestBody.variables?.filter).toContain('office_state:TX');
  271 | 
  272 |     const json = await response.json();
  273 |     const results = json.data?.browseEntities?.results || [];
  274 |     expect(results.length).toBeGreaterThan(0);
  275 |   });
  276 | 
  277 |   test('state profile candidates sorted by total contributions descending', async ({ page }) => {
  278 |     const responsePromise = page.waitForResponse(resp => {
  279 |       if (!resp.url().includes('/api/graphql')) return false;
  280 |       if (resp.request().method() !== 'POST') return false;
  281 |       const body = resp.request().postData() || '';
  282 |       return body.includes('browseEntities');
  283 |     }, { timeout: DATA_LOAD_TIMEOUT });
  284 | 
  285 |     await page.goto(TEXAS_PATH);
  286 |     const response = await responsePromise;
  287 | 
  288 |     const json = await response.json();
  289 |     const results = json.data?.browseEntities?.results || [];
  290 |     expect(results.length).toBeGreaterThan(1);
  291 | 
  292 |     const contributions = results.map((r: { metadata?: string }) => {
  293 |       try {
  294 |         const meta = JSON.parse(r.metadata || '{}');
  295 |         return meta.total_contributions || 0;
  296 |       } catch { return 0; }
  297 |     });
  298 | 
  299 |     for (let i = 1; i < contributions.length; i++) {
  300 |       expect(contributions[i - 1]).toBeGreaterThanOrEqual(contributions[i]);
  301 |     }
  302 |   });
  303 | });
  304 | 
  305 | test.describe('district profile - dynamic candidates', () => {
  306 |   const DISTRICT_PATH = '/districts/tx-10/';
  307 | 
  308 |   test('district profile loads candidates filtered to district', async ({ page }) => {
  309 |     const responsePromise = page.waitForResponse(resp => {
  310 |       if (!resp.url().includes('/api/graphql')) return false;
  311 |       if (resp.request().method() !== 'POST') return false;
  312 |       const body = resp.request().postData() || '';
  313 |       return body.includes('browseEntities');
  314 |     }, { timeout: DATA_LOAD_TIMEOUT });
  315 | 
  316 |     await page.goto(DISTRICT_PATH);
  317 |     const response = await responsePromise;
  318 | 
  319 |     const requestBody = JSON.parse(response.request().postData() || '{}');
  320 |     const filter = requestBody.variables?.filter || '';
  321 |     expect(filter).toContain('office:H');
  322 |     expect(filter).toContain('office_state:TX');
  323 |     expect(filter).toContain('office_district:10');
  324 | 
  325 |     const json = await response.json();
  326 |     const results = json.data?.browseEntities?.results || [];
  327 |     expect(results.length).toBeGreaterThan(0);
  328 |   });
  329 | 
  330 |   test('district profile candidates sorted by total contributions descending', async ({ page }) => {
  331 |     const responsePromise = page.waitForResponse(resp => {
  332 |       if (!resp.url().includes('/api/graphql')) return false;
  333 |       if (resp.request().method() !== 'POST') return false;
  334 |       const body = resp.request().postData() || '';
  335 |       return body.includes('browseEntities');
  336 |     }, { timeout: DATA_LOAD_TIMEOUT });
  337 | 
  338 |     await page.goto(DISTRICT_PATH);
  339 |     const response = await responsePromise;
  340 | 
  341 |     const json = await response.json();
  342 |     const results = json.data?.browseEntities?.results || [];
  343 |     expect(results.length).toBeGreaterThan(1);
  344 | 
  345 |     const contributions = results.map((r: { metadata?: string }) => {
  346 |       try {
  347 |         const meta = JSON.parse(r.metadata || '{}');
  348 |         return meta.total_contributions || 0;
  349 |       } catch { return 0; }
  350 |     });
  351 | 
  352 |     for (let i = 1; i < contributions.length; i++) {
  353 |       expect(contributions[i - 1]).toBeGreaterThanOrEqual(contributions[i]);
  354 |     }
  355 |   });
  356 | });
  357 | 
  358 | // Helper to intercept browseEntities GraphQL response and extract score values
  359 | async function getBrowseScores(page: import('@playwright/test').Page, url: string): Promise<number[]> {
> 360 |   const responsePromise = page.waitForResponse(resp => {
      |                                ^ TimeoutError: page.waitForResponse: Timeout 15000ms exceeded while waiting for event "response"
  361 |     if (!resp.url().includes('/api/graphql')) return false;
  362 |     if (resp.request().method() !== 'POST') return false;
  363 |     const body = resp.request().postData() || '';
  364 |     return body.includes('browseEntities');
  365 |   }, { timeout: DATA_LOAD_TIMEOUT });
  366 |   await page.goto(url);
  367 |   const response = await responsePromise;
  368 |   const json = await response.json();
  369 |   const results = json.data?.browseEntities?.results || [];
  370 |   return results.map((r: { score?: number }) => r.score || 0);
  371 | }
  372 | 
  373 | test.describe('entity browse sort order - quantitative fields', () => {
  374 | 
  375 |   test('candidates sorted by total contributions descending', async ({ page }) => {
  376 |     const scores = await getBrowseScores(page, '/candidates/');
  377 |     expect(scores.length).toBeGreaterThan(1);
  378 |     for (let i = 1; i < scores.length; i++) {
  379 |       expect(scores[i - 1]).toBeGreaterThanOrEqual(scores[i]);
  380 |     }
  381 |   });
  382 | 
  383 |   test('committees sorted by total receipts descending', async ({ page }) => {
  384 |     const scores = await getBrowseScores(page, '/committees/');
  385 |     expect(scores.length).toBeGreaterThan(1);
  386 |     for (let i = 1; i < scores.length; i++) {
  387 |       expect(scores[i - 1]).toBeGreaterThanOrEqual(scores[i]);
  388 |     }
  389 |   });
  390 | 
  391 |   test('vendors sorted by total received descending', async ({ page }) => {
  392 |     const scores = await getBrowseScores(page, '/vendors/');
  393 |     expect(scores.length).toBeGreaterThan(1);
  394 |     for (let i = 1; i < scores.length; i++) {
  395 |       expect(scores[i - 1]).toBeGreaterThanOrEqual(scores[i]);
  396 |     }
  397 |   });
  398 | 
  399 |   test('individuals sorted by total contributions descending', async ({ page, baseURL }) => {
  400 |     await premiumVisitor(page.context(), baseURL); // donor records are premium (sites#277)
  401 |     const scores = await getBrowseScores(page, '/individuals/');
  402 |     expect(scores.length).toBeGreaterThan(1);
  403 |     // A stale search index scores every donor 0 and the order means nothing.
  404 |     expect(scores[0], 'top individual donor should have contributions').toBeGreaterThan(0);
  405 |     for (let i = 1; i < scores.length; i++) {
  406 |       expect(scores[i - 1]).toBeGreaterThanOrEqual(scores[i]);
  407 |     }
  408 |   });
  409 | 
  410 |   test('employers sorted by total employee contributions descending', async ({ page }) => {
  411 |     const scores = await getBrowseScores(page, '/employers/');
  412 |     expect(scores.length).toBeGreaterThan(1);
  413 |     for (let i = 1; i < scores.length; i++) {
  414 |       expect(scores[i - 1]).toBeGreaterThanOrEqual(scores[i]);
  415 |     }
  416 |   });
  417 | });
  418 | 
  419 | test.describe('us.elect.info entity sections - additional index + detail', () => {
  420 | 
  421 |   test('employers section - index and random pages', async ({ page }) => {
  422 |     const indexResponse = await page.goto('/employers/');
  423 |     expect(indexResponse?.status(), '/employers/ should answer 200').toBe(200);
  424 |     await expect(page.locator('h1')).toContainText(/Employers/i);
  425 | 
  426 |     await expect(page.locator('.entity-search-result').first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  427 | 
  428 |     const links = await page.locator('.entity-search-result').all();
  429 |     expect(links.length).toBeGreaterThan(0);
  430 | 
  431 |     const hrefs = await Promise.all(links.slice(0, 20).map(l => l.getAttribute('href')));
  432 |     const randomLinks = getRandomItems(hrefs.filter(Boolean) as string[], 3);
  433 | 
  434 |     for (const href of randomLinks) {
  435 |       const entityResponse = await page.goto(toRelativePath(href));
  436 |       expect(entityResponse?.status(), `${toRelativePath(href)} should answer 200`).toBe(200);
  437 |       await expect(page.locator('h1')).toBeVisible();
  438 |     }
  439 |   });
  440 | 
  441 |   test('vendors section - index and random pages', async ({ page }) => {
  442 |     const indexResponse = await page.goto('/vendors/');
  443 |     expect(indexResponse?.status(), '/vendors/ should answer 200').toBe(200);
  444 |     await expect(page.locator('h1')).toContainText(/Vendors/i);
  445 | 
  446 |     await expect(page.locator('.entity-search-result').first()).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  447 | 
  448 |     const links = await page.locator('.entity-search-result').all();
  449 |     expect(links.length).toBeGreaterThan(0);
  450 | 
  451 |     const hrefs = await Promise.all(links.slice(0, 20).map(l => l.getAttribute('href')));
  452 |     const randomLinks = getRandomItems(hrefs.filter(Boolean) as string[], 3);
  453 | 
  454 |     for (const href of randomLinks) {
  455 |       const entityResponse = await page.goto(toRelativePath(href));
  456 |       expect(entityResponse?.status(), `${toRelativePath(href)} should answer 200`).toBe(200);
  457 |       await expect(page.locator('h1')).toBeVisible();
  458 |     }
  459 |   });
  460 | 
```