# 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 >> district profile - dynamic candidates >> district profile candidates sorted by total contributions descending
- Location: tests/ui/entities.spec.ts:336:7

# Error details

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

```
Error: response.json: Test timeout of 30000ms exceeded.
```

# Page snapshot

```yaml
- generic [active] [ref=e1]:
  - banner [ref=e2]:
    - heading "Texas Congressional District 10" [level=1] [ref=e3]
    - paragraph [ref=e4]: Congressional District Profile
  - generic [ref=e5]:
    - heading "Overview" [level=2] [ref=e6]
    - generic [ref=e7]:
      - term [ref=e8]: District ID
      - definition [ref=e9]: TX-10
      - term [ref=e10]: State
      - definition [ref=e11]:
        - link "Texas" [ref=e12] [cursor=pointer]:
          - /url: https://us.elect.info/states/texas/
      - term [ref=e13]: District Number
      - definition [ref=e14]: "10"
      - term [ref=e15]: Total Candidates
      - definition [ref=e16]: "109"
      - term [ref=e17]: Election Cycles
      - definition [ref=e18]:
        - generic [ref=e19]:
          - link "2026" [ref=e20] [cursor=pointer]:
            - /url: https://us.elect.info/elections/2026/
          - text: ","
          - link "2024" [ref=e21] [cursor=pointer]:
            - /url: https://us.elect.info/elections/2024/
          - text: ","
          - link "2022" [ref=e22] [cursor=pointer]:
            - /url: https://us.elect.info/elections/2022/
          - text: ","
          - link "2020" [ref=e23] [cursor=pointer]:
            - /url: https://us.elect.info/elections/2020/
          - text: ","
          - link "2018" [ref=e24] [cursor=pointer]:
            - /url: https://us.elect.info/elections/2018/
          - text: ","
          - link "2016" [ref=e25] [cursor=pointer]:
            - /url: https://us.elect.info/elections/2016/
          - text: ","
          - link "2014" [ref=e26] [cursor=pointer]:
            - /url: https://us.elect.info/elections/2014/
          - text: ","
          - link "2012" [ref=e27] [cursor=pointer]:
            - /url: https://us.elect.info/elections/2012/
          - text: ","
          - link "2010" [ref=e28] [cursor=pointer]:
            - /url: https://us.elect.info/elections/2010/
          - text: ","
          - link "2008" [ref=e29] [cursor=pointer]:
            - /url: https://us.elect.info/elections/2008/
          - text: ","
          - link "2006" [ref=e30] [cursor=pointer]:
            - /url: https://us.elect.info/elections/2006/
          - text: ","
          - link "2004" [ref=e31] [cursor=pointer]:
            - /url: https://us.elect.info/elections/2004/
          - text: ","
          - link "2002" [ref=e32] [cursor=pointer]:
            - /url: https://us.elect.info/elections/2002/
          - text: ","
          - link "2000" [ref=e33] [cursor=pointer]:
            - /url: https://us.elect.info/elections/2000/
          - text: ","
          - link "1998" [ref=e34] [cursor=pointer]:
            - /url: https://us.elect.info/elections/1998/
          - text: ","
          - link "1996" [ref=e35] [cursor=pointer]:
            - /url: https://us.elect.info/elections/1996/
          - text: ","
          - link "1994" [ref=e36] [cursor=pointer]:
            - /url: https://us.elect.info/elections/1994/
          - text: ","
          - link "1992" [ref=e37] [cursor=pointer]:
            - /url: https://us.elect.info/elections/1992/
          - text: ","
          - link "1990" [ref=e38] [cursor=pointer]:
            - /url: https://us.elect.info/elections/1990/
          - text: ","
          - link "1988" [ref=e39] [cursor=pointer]:
            - /url: https://us.elect.info/elections/1988/
          - text: ","
          - link "1986" [ref=e40] [cursor=pointer]:
            - /url: https://us.elect.info/elections/1986/
          - text: ","
          - link "1984" [ref=e41] [cursor=pointer]:
            - /url: https://us.elect.info/elections/1984/
          - text: ","
          - link "1982" [ref=e42] [cursor=pointer]:
            - /url: https://us.elect.info/elections/1982/
          - text: ","
          - link "1980" [ref=e43] [cursor=pointer]:
            - /url: https://us.elect.info/elections/1980/
    - paragraph [ref=e44]:
      - link "View office details for U.S. Representative - Texas District 10" [ref=e45] [cursor=pointer]:
        - /url: https://us.elect.info/offices/house/tx-10/
  - generic [ref=e46]:
    - heading "Demographics" [level=2] [ref=e47]
    - generic [ref=e48]:
      - term [ref=e49]: Total Citizen Population
      - definition [ref=e50]: 738,927
      - term [ref=e51]: Citizen Voting Age Population (CVAP)
      - definition [ref=e52]: 565,043
    - heading "Citizen Population by Race/Ethnicity" [level=3] [ref=e53]
    - table [ref=e54]:
      - rowgroup [ref=e55]:
        - row [ref=e56]:
          - columnheader "Group" [ref=e57]
          - columnheader "Citizens" [ref=e58]
          - columnheader "Share" [ref=e59]
      - rowgroup [ref=e60]:
        - row [ref=e61]:
          - cell "Non-Hispanic" [ref=e62]
          - cell "570,069" [ref=e63]
          - cell "77.1%" [ref=e64]
        - row [ref=e65]:
          - cell "Hispanic" [ref=e66]
          - cell "168,860" [ref=e67]
          - cell "22.9%" [ref=e68]
        - row [ref=e69]:
          - cell "White" [ref=e70]
          - cell "441,386" [ref=e71]
          - cell "59.7%" [ref=e72]
        - row [ref=e73]:
          - cell "Black" [ref=e74]
          - cell "76,398" [ref=e75]
          - cell "10.3%" [ref=e76]
        - row [ref=e77]:
          - cell "Asian" [ref=e78]
          - cell "29,999" [ref=e79]
          - cell "4.1%" [ref=e80]
        - row [ref=e81]:
          - cell "American Indian / Alaska Native" [ref=e82]
          - cell "918" [ref=e83]
          - cell "0.1%" [ref=e84]
        - row [ref=e85]:
          - cell "Native Hawaiian / Pacific Islander" [ref=e86]
          - cell "401" [ref=e87]
          - cell "0.1%" [ref=e88]
        - row [ref=e89]:
          - cell "Two or More Races" [ref=e90]
          - cell "1,986" [ref=e91]
          - cell "0.3%" [ref=e92]
    - heading "Citizen Voting Age Population by Race/Ethnicity" [level=3] [ref=e93]
    - table [ref=e94]:
      - rowgroup [ref=e95]:
        - row [ref=e96]:
          - columnheader "Group" [ref=e97]
          - columnheader "CVAP" [ref=e98]
          - columnheader "Share" [ref=e99]
      - rowgroup [ref=e100]:
        - row [ref=e101]:
          - cell "Non-Hispanic" [ref=e102]
          - cell "458,048" [ref=e103]
          - cell "81.1%" [ref=e104]
        - row [ref=e105]:
          - cell "Hispanic" [ref=e106]
          - cell "106,999" [ref=e107]
          - cell "18.9%" [ref=e108]
        - row [ref=e109]:
          - cell "White" [ref=e110]
          - cell "363,023" [ref=e111]
          - cell "64.2%" [ref=e112]
        - row [ref=e113]:
          - cell "Black" [ref=e114]
          - cell "63,314" [ref=e115]
          - cell "11.2%" [ref=e116]
        - row [ref=e117]:
          - cell "Asian" [ref=e118]
          - cell "24,369" [ref=e119]
          - cell "4.3%" [ref=e120]
        - row [ref=e121]:
          - cell "American Indian / Alaska Native" [ref=e122]
          - cell "5,921" [ref=e123]
          - cell "1.0%" [ref=e124]
        - row [ref=e125]:
          - cell "Native Hawaiian / Pacific Islander" [ref=e126]
          - cell "372" [ref=e127]
          - cell "0.1%" [ref=e128]
        - row [ref=e129]:
          - cell "Two or More Races" [ref=e130]
          - cell "1,389" [ref=e131]
          - cell "0.2%" [ref=e132]
    - paragraph [ref=e133]: "Source: Redistricting Data Hub / U.S. Census Bureau CVAP Special Tabulation."
  - generic [ref=e134]:
    - heading "Candidates" [level=2] [ref=e135]
    - generic [ref=e137]:
      - generic [ref=e138]:
        - textbox "Search candidates for this district..." [ref=e139]
        - button "Search" [ref=e140] [cursor=pointer]
      - generic [ref=e141]:
        - heading "Candidates" [level=3] [ref=e143]
        - paragraph [ref=e145]: "Failed to load: 400"
  - contentinfo [ref=e146]:
    - paragraph [ref=e147]:
      - link "elect.info" [ref=e148] [cursor=pointer]:
        - /url: https://us.elect.info/
      - text: "| Data from"
      - link "Federal Election Commission" [ref=e149] [cursor=pointer]:
        - /url: https://us.elect.info/datasources/fec.gov/
      - text: "| Generated 2026-10-09"
```

# Test source

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