# 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/vendor-purposes.spec.ts >> vendor purpose card — section and query >> #purposes section is rendered on a vendor detail page
- Location: tests/ui/vendor-purposes.spec.ts:89:7

# Error details

```
Error: page.goto: net::ERR_CONNECTION_REFUSED at http://10.1.105.22/vendors/vendor-purpose-test/
Call log:
  - navigating to "http://10.1.105.22/vendors/vendor-purpose-test/", waiting until "load"

```

# Test source

```ts
  1   | import { test, expect, type Page } from '@playwright/test';
  2   | 
  3   | // Vendor purpose taxonomy card (sites#349).
  4   | // mountVendorPurposes() is called by loadVendorDetail() and renders a
  5   | // #purposes section with a cycle selector, filter input, sortable table,
  6   | // and a Sankey | Table toggle (same .mf-view-btn pattern as #clients).
  7   | // All tests mock /api/graphql and /_resolve so they run without a live cluster.
  8   | 
  9   | const DATA_LOAD_TIMEOUT = 20_000;
  10  | 
  11  | const VENDOR_ID = 'vendor-purpose-test';
  12  | const VENDOR_NAME = 'TEST MEDIA VENDOR';
  13  | 
  14  | const PURPOSE_DATA = {
  15  |   taxonomyVersion: 'v1.2',
  16  |   totalAmount: 10_000_000,
  17  |   groups: [
  18  |     {
  19  |       group: 'Media Buying', amount: 6_000_000, count: 120, clients: 8,
  20  |       topPurposes: [
  21  |         { purpose: 'MEDIA BUY', amount: 5_000_000, count: 100 },
  22  |         { purpose: 'AD PLACEMENT', amount: 1_000_000, count: 20 },
  23  |       ],
  24  |     },
  25  |     {
  26  |       group: 'Digital', amount: 3_000_000, count: 60, clients: 5,
  27  |       topPurposes: [
  28  |         { purpose: 'DIGITAL ADS', amount: 3_000_000, count: 60 },
  29  |       ],
  30  |     },
  31  |     {
  32  |       group: 'Consulting', amount: 1_000_000, count: 20, clients: 3,
  33  |       topPurposes: [
  34  |         { purpose: 'POLITICAL CONSULTING', amount: 1_000_000, count: 20 },
  35  |       ],
  36  |     },
  37  |   ],
  38  |   links: [
  39  |     { committeeId: 'c-dem', name: 'DEM VICTORY FUND', party: 'DEM', affinityDem: null, affinityRep: null, group: 'Media Buying', amount: 4_000_000, count: 80 },
  40  |     { committeeId: 'c-rep', name: 'REP LEADERSHIP PAC', party: null, affinityDem: 0.05, affinityRep: 0.95, group: 'Digital', amount: 3_000_000, count: 60 },
  41  |   ],
  42  |   otherLinks: [
  43  |     { group: 'Media Buying', amount: 2_000_000, count: 40 },
  44  |     { group: 'Consulting', amount: 1_000_000, count: 20 },
  45  |   ],
  46  | };
  47  | 
  48  | const VENDOR_ROW = {
  49  |   id: VENDOR_ID,
  50  |   name: VENDOR_NAME,
  51  |   name_normalized: 'test media vendor',
  52  |   city: 'AUSTIN',
  53  |   state: 'TX',
  54  |   total_received: 10_000_000,
  55  |   payment_count: 180,
  56  |   purpose_categories: 'MEDIA',
  57  |   client_count: 12,
  58  |   first_payment: '2020-01-01',
  59  |   last_payment: '2026-01-01',
  60  |   cycles: '2020,2022,2024,2026',
  61  | };
  62  | 
  63  | type PurposeOverride = typeof PURPOSE_DATA | null | undefined;
  64  | 
  65  | /** Wire mocks for a vendor page. Returns the array that receives captured request bodies. */
  66  | async function installMocks(page: Page, purposeOverride: PurposeOverride = undefined): Promise<string[]> {
  67  |   const graphqlBodies: string[] = [];
  68  |   await page.route('**/_resolve/**', route => route.fulfill({ status: 404, body: '' }));
  69  |   await page.route('**/api/graphql', async route => {
  70  |     const body = route.request().postData() || '';
  71  |     graphqlBodies.push(body);
  72  |     let data: unknown;
  73  |     if (body.includes('VendorPurposeFlow')) {
  74  |       data = {
  75  |         vendorPurposeFlow: purposeOverride === undefined ? PURPOSE_DATA : purposeOverride,
  76  |       };
  77  |     } else if (body.includes('VendorRankedClients')) {
  78  |       data = { vendorClients: { ranked: false, clientCount: null, totalReceived: null, otherCount: null, otherAmount: null, clients: [] } };
  79  |     } else {
  80  |       // VendorProfile (and any other vendor query)
  81  |       data = { vendors: [VENDOR_ROW] };
  82  |     }
  83  |     await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data }) });
  84  |   });
  85  |   return graphqlBodies;
  86  | }
  87  | 
  88  | test.describe('vendor purpose card — section and query', () => {
  89  |   test('#purposes section is rendered on a vendor detail page', async ({ page }) => {
  90  |     const errors: string[] = [];
  91  |     page.on('pageerror', e => errors.push(String(e)));
  92  |     await installMocks(page);
  93  | 
> 94  |     const response = await page.goto(`/vendors/${VENDOR_ID}/`);
      |                                 ^ Error: page.goto: net::ERR_CONNECTION_REFUSED at http://10.1.105.22/vendors/vendor-purpose-test/
  95  |     expect(response?.status()).toBe(200);
  96  |     await expect(page.locator('.leaf-detail-page h1')).toHaveText(VENDOR_NAME, { timeout: DATA_LOAD_TIMEOUT });
  97  |     await expect(page.locator('#purposes')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  98  |     expect(errors).toEqual([]);
  99  |   });
  100 | 
  101 |   test('VendorPurposeFlow query is issued with id and null cycle on initial load', async ({ page }) => {
  102 |     const graphqlBodies = await installMocks(page);
  103 | 
  104 |     await page.goto(`/vendors/${VENDOR_ID}/`);
  105 |     await expect(page.locator('#purposes')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  106 | 
  107 |     const purposeBody = graphqlBodies.find(b => b.includes('VendorPurposeFlow'));
  108 |     expect(purposeBody, 'VendorPurposeFlow query not issued').toBeTruthy();
  109 |     const parsed = JSON.parse(purposeBody!);
  110 |     expect(parsed.variables?.id, 'vendor id variable').toBe(VENDOR_ID);
  111 |     // initial load: cycle must be absent or null — not a specific year
  112 |     expect(parsed.variables?.cycle ?? null, 'initial cycle variable').toBeNull();
  113 |   });
  114 | 
  115 |   test('query operation name is VendorPurposeFlow', async ({ page }) => {
  116 |     const graphqlBodies = await installMocks(page);
  117 | 
  118 |     await page.goto(`/vendors/${VENDOR_ID}/`);
  119 |     await expect(page.locator('#purposes')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  120 | 
  121 |     const purposeBody = graphqlBodies.find(b => b.includes('VendorPurposeFlow'));
  122 |     expect(purposeBody).toContain('query VendorPurposeFlow(');
  123 |   });
  124 | });
  125 | 
  126 | test.describe('vendor purpose card — cycle selector', () => {
  127 |   test('cycle selector is present with aria-label "Purpose cycle"', async ({ page }) => {
  128 |     await installMocks(page);
  129 | 
  130 |     await page.goto(`/vendors/${VENDOR_ID}/`);
  131 |     await expect(page.locator('#purposes')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  132 | 
  133 |     await expect(page.locator('#purposes select[aria-label="Purpose cycle"]')).toBeVisible();
  134 |   });
  135 | 
  136 |   test('cycle selector has an "All cycles" option as the first entry', async ({ page }) => {
  137 |     await installMocks(page);
  138 | 
  139 |     await page.goto(`/vendors/${VENDOR_ID}/`);
  140 |     await expect(page.locator('#purposes select[aria-label="Purpose cycle"]')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  141 | 
  142 |     const options = page.locator('#purposes select[aria-label="Purpose cycle"] option');
  143 |     await expect(options.first()).toHaveText('All cycles');
  144 |     await expect(options.first()).toHaveAttribute('value', '');
  145 |   });
  146 | 
  147 |   test('cycle options are populated from the vendor cycles field', async ({ page }) => {
  148 |     await installMocks(page);
  149 | 
  150 |     await page.goto(`/vendors/${VENDOR_ID}/`);
  151 |     await expect(page.locator('#purposes select[aria-label="Purpose cycle"]')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  152 | 
  153 |     // VENDOR_ROW.cycles = '2020,2022,2024,2026'
  154 |     for (const year of ['2020', '2022', '2024', '2026']) {
  155 |       await expect(page.locator(`#purposes select[aria-label="Purpose cycle"] option[value="${year}"]`), `missing ${year} option`).toHaveCount(1);
  156 |     }
  157 |   });
  158 | 
  159 |   test('selecting a cycle issues a new VendorPurposeFlow query with that cycle', async ({ page }) => {
  160 |     const graphqlBodies = await installMocks(page);
  161 | 
  162 |     await page.goto(`/vendors/${VENDOR_ID}/`);
  163 |     await expect(page.locator('#purposes')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  164 | 
  165 |     const select = page.locator('#purposes select[aria-label="Purpose cycle"]');
  166 |     await expect(select).toBeVisible();
  167 |     const countBefore = graphqlBodies.filter(b => b.includes('VendorPurposeFlow')).length;
  168 | 
  169 |     await select.selectOption('2024');
  170 |     // wait for the status element to clear (loading finished)
  171 |     await expect(page.locator('#purposes [role="status"]')).toHaveText('', { timeout: DATA_LOAD_TIMEOUT });
  172 | 
  173 |     const purposeBodies = graphqlBodies.filter(b => b.includes('VendorPurposeFlow'));
  174 |     expect(purposeBodies.length, 'cycle change did not issue a second query').toBeGreaterThan(countBefore);
  175 |     const last = JSON.parse(purposeBodies[purposeBodies.length - 1]);
  176 |     expect(last.variables?.cycle).toBe(2024);
  177 |   });
  178 | 
  179 |   test('switching back to All cycles sends cycle: null', async ({ page }) => {
  180 |     const graphqlBodies = await installMocks(page);
  181 | 
  182 |     await page.goto(`/vendors/${VENDOR_ID}/`);
  183 |     const select = page.locator('#purposes select[aria-label="Purpose cycle"]');
  184 |     await expect(select).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  185 | 
  186 |     await select.selectOption('2024');
  187 |     await expect(page.locator('#purposes [role="status"]')).toHaveText('', { timeout: DATA_LOAD_TIMEOUT });
  188 | 
  189 |     await select.selectOption('');
  190 |     await expect(page.locator('#purposes [role="status"]')).toHaveText('', { timeout: DATA_LOAD_TIMEOUT });
  191 | 
  192 |     const last = JSON.parse(graphqlBodies.filter(b => b.includes('VendorPurposeFlow')).pop()!);
  193 |     expect(last.variables?.cycle ?? null).toBeNull();
  194 |   });
```