# 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 — cycle selector >> cycle selector is present with aria-label "Purpose cycle"
- Location: tests/ui/vendor-purposes.spec.ts:127:7

# Error details

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

```
Error: page.goto: net::ERR_ABORTED; maybe frame was detached?
Call log:
  - navigating to "http://10.1.105.75/vendors/vendor-purpose-test/", waiting until "load"

```

# Test source

```ts
  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}/`);
  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}/`);
      |                ^ Error: page.goto: net::ERR_ABORTED; maybe frame was detached?
  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 |   });
  195 | });
  196 | 
  197 | test.describe('vendor purpose card — filter input', () => {
  198 |   test.beforeEach(async ({ page }) => {
  199 |     await page.addInitScript(() => localStorage.setItem('electinfo:money-flow-view', 'table'));
  200 |   });
  201 |   test('filter input has aria-label "Filter purpose groups"', async ({ page }) => {
  202 |     await installMocks(page);
  203 | 
  204 |     await page.goto(`/vendors/${VENDOR_ID}/`);
  205 |     await expect(page.locator('#purposes tbody tr')).toHaveCount(3, { timeout: DATA_LOAD_TIMEOUT });
  206 | 
  207 |     await expect(page.locator('#purposes input[aria-label="Filter purpose groups"]')).toBeVisible();
  208 |   });
  209 | 
  210 |   test('typing in the filter narrows visible rows by group name', async ({ page }) => {
  211 |     await installMocks(page);
  212 | 
  213 |     await page.goto(`/vendors/${VENDOR_ID}/`);
  214 |     await expect(page.locator('#purposes tbody tr')).toHaveCount(3, { timeout: DATA_LOAD_TIMEOUT });
  215 | 
  216 |     await page.locator('#purposes input[aria-label="Filter purpose groups"]').fill('Media');
  217 |     await expect(page.locator('#purposes tbody tr')).toHaveCount(1);
  218 |     await expect(page.locator('#purposes tbody tr').first()).toContainText('Media Buying');
  219 |   });
  220 | 
  221 |   test('filter matches topPurpose text as well as the group name', async ({ page }) => {
  222 |     await installMocks(page);
  223 | 
  224 |     await page.goto(`/vendors/${VENDOR_ID}/`);
  225 |     await expect(page.locator('#purposes tbody tr')).toHaveCount(3, { timeout: DATA_LOAD_TIMEOUT });
  226 | 
  227 |     await page.locator('#purposes input[aria-label="Filter purpose groups"]').fill('DIGITAL ADS');
  228 |     await expect(page.locator('#purposes tbody tr')).toHaveCount(1);
  229 |     await expect(page.locator('#purposes tbody tr').first()).toContainText('Digital');
  230 |   });
```