# 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-purpose-lifecycle.spec.ts >> unavailable cycle request error retains raw disclosure
- Location: tests/ui/vendor-purpose-lifecycle.spec.ts:25:7

# Error details

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

```

# Test source

```ts
  1   | import { test, expect, type Page, type Route } from '@playwright/test';
  2   | 
  3   | const flow = (group = 'Digital') => ({
  4   |   taxonomyVersion: 'kw-1', totalAmount: 100,
  5   |   groups: [{ group, amount: 100, count: 2, clients: 1, topPurposes: [{ purpose: '<DIGITAL ADS>', amount: 100, count: 2 }] }],
  6   |   links: [{ committeeId: 'c1', name: 'Committee', party: 'DEM', affinityDem: null, affinityRep: null, group, amount: 100, count: 2 }],
  7   |   otherLinks: [],
  8   | });
  9   | 
  10  | async function mock(page: Page, purpose: (route: Route, cycle: number | null) => Promise<void>) {
  11  |   await page.route('**/_resolve/**', route => route.fulfill({ status: 404, body: '' }));
  12  |   await page.route('**/api/graphql', async route => {
  13  |     const { query, variables } = route.request().postDataJSON();
  14  |     if (query.includes('VendorPurposeFlow')) return purpose(route, variables.cycle);
  15  |     const data = query.includes('VendorProfile') ? { vendors: [{
  16  |       id: 'purpose-lifecycle', name: 'Purpose lifecycle vendor', purpose_categories: '<RAW PURPOSE>',
  17  |       cycles: '2022,2024', client_count: 0,
  18  |     }] } : {};
  19  |     await route.fulfill({ json: { data } });
  20  |   });
  21  | }
  22  | const respond = (route: Route, data: ReturnType<typeof flow>) => route.fulfill({ json: { data: { vendorPurposeFlow: data } } });
  23  | 
  24  | for (const failed of [false, true]) {
  25  |   test(`unavailable cycle ${failed ? 'request error' : 'empty groups'} retains raw disclosure`, async ({ page }) => {
  26  |     await mock(page, async (route, cycle) => {
  27  |       if (!cycle) return respond(route, flow());
  28  |       if (failed) return route.fulfill({ status: 503, body: 'Unavailable' });
  29  |       await route.fulfill({ json: { data: { vendorPurposeFlow: { taxonomyVersion: 'kw-1', totalAmount: 0, groups: [], links: [], otherLinks: [] } } } });
  30  |     });
> 31  |     await page.goto('/vendors/purpose-lifecycle/');
      |                ^ Error: page.goto: net::ERR_CONNECTION_REFUSED at http://10.1.105.22/vendors/purpose-lifecycle/
  32  |     await expect(page.locator('#purposes canvas')).toBeVisible();
  33  |     await page.getByLabel('Purpose cycle').selectOption('2024');
  34  |     await expect(page.locator('#purposes')).toContainText('Cycle breakdown unavailable.');
  35  |     await expect(page.locator('#purposes table')).toHaveCount(0);
  36  |     await expect(page.locator('#purposes canvas')).toHaveCount(0);
  37  |     await page.locator('#purposes summary').click();
  38  |     await expect(page.locator('#purposes .purpose-raw')).toContainText('<RAW PURPOSE>');
  39  |     expect(await page.locator('#purposes .purpose-raw').evaluate(el => el.querySelector('raw') !== null)).toBe(false);
  40  |   });
  41  | }
  42  | 
  43  | test('latest cycle wins over a slow previous request, without duplicate charts', async ({ page }) => {
  44  |   let release!: () => void;
  45  |   const gate = new Promise<void>(resolve => { release = resolve; });
  46  |   let slowStarted!: () => void;
  47  |   const started = new Promise<void>(resolve => { slowStarted = resolve; });
  48  |   await mock(page, async (route, cycle) => {
  49  |     if (cycle === 2022) { slowStarted(); await gate; return respond(route, flow('Stale group')); }
  50  |     return respond(route, flow(cycle === 2024 ? 'Latest group' : 'All cycles group'));
  51  |   });
  52  |   await page.goto('/vendors/purpose-lifecycle/');
  53  |   await expect(page.locator('#purposes canvas')).toBeVisible();
  54  |   await page.getByLabel('Purpose cycle').selectOption('2022');
  55  |   await started;
  56  |   await page.getByLabel('Purpose cycle').selectOption('2024');
  57  |   await expect(page.locator('#purposes tbody')).toContainText('Latest group');
  58  |   const lateResponse = page.waitForResponse(response => response.url().includes('/api/graphql') && response.request().postDataJSON().variables?.cycle === 2022);
  59  |   release();
  60  |   await lateResponse;
  61  |   // A successful subsequent request must still mount once, not resurrect the old card.
  62  |   await expect(page.locator('#purposes tbody')).not.toContainText('Stale group');
  63  |   await expect(page.locator('#purposes .mf-toggle')).toHaveCount(1);
  64  |   await expect(page.locator('#purposes canvas')).toHaveCount(1);
  65  |   await page.getByLabel('Purpose cycle').selectOption('');
  66  |   await expect(page.locator('#purposes tbody')).toContainText('All cycles group');
  67  |   await expect(page.locator('#purposes .mf-toggle')).toHaveCount(1);
  68  |   await expect(page.locator('#purposes canvas')).toHaveCount(1);
  69  | });
  70  | 
  71  | test('top-purpose text is escaped in the expandable table', async ({ page }) => {
  72  |   await mock(page, async route => respond(route, flow()));
  73  |   await page.goto('/vendors/purpose-lifecycle/');
  74  |   await expect(page.locator('#purposes canvas')).toBeVisible();
  75  |   await page.locator('#purposes .mf-view-btn[data-view="table"]').click();
  76  |   await page.locator('#purposes tbody summary').click();
  77  |   await expect(page.locator('#purposes .purpose-raw-row')).toContainText('<DIGITAL ADS>');
  78  |   await expect(page.locator('#purposes tbody digital')).toHaveCount(0);
  79  | });
  80  | 
  81  | test('signed group amounts use positive shares and stable IDs without negative ribbons', async ({ page }) => {
  82  |   await mock(page, async route => {
  83  |     const data = flow('Meals & food');
  84  |     await route.fulfill({ json: { data: { vendorPurposeFlow: {
  85  |       ...data, totalAmount: 80,
  86  |       groups: [
  87  |         { ...data.groups[0], groupId: 'meals_food' },
  88  |         { groupId: 'card_reimbursements', group: 'Card payments & reimbursements', amount: -20, count: 1, clients: 1, topPurposes: [] },
  89  |       ],
  90  |       links: [
  91  |         { ...data.links[0], groupId: 'meals_food' },
  92  |         { ...data.links[0], committeeId: 'refund', groupId: 'card_reimbursements', group: 'Card payments & reimbursements', amount: -20 },
  93  |       ],
  94  |     } } } });
  95  |   });
  96  |   await page.goto('/vendors/purpose-lifecycle/');
  97  |   await expect(page.locator('#purposes canvas')).toBeVisible();
  98  |   await expect(page.locator('#overview dt')).toContainText(['Name', 'Normalized name', 'Location', 'Top purposes']);
  99  |   const chart = await page.locator('#purposes .money-flow-chart').evaluate(el => window.electinfoMoneyFlow!.inspect!(el.parentElement!)!.option as any);
  100 |   expect(chart.series[0].data.some((n: any) => n.name === 'purpose:meals_food')).toBe(true);
  101 |   expect(chart.series[0].links.every((l: any) => l.value > 0)).toBe(true);
  102 |   await page.locator('#purposes [data-view="table"]').click();
  103 |   await expect(page.locator('#purposes tr[data-group-id="meals_food"]')).toContainText('100.0%');
  104 |   const refunds = page.locator('#purposes tr[data-group-id="card_reimbursements"]');
  105 |   await expect(refunds).toContainText('-$20.00');
  106 |   await expect(refunds).toContainText('-20.0%');
  107 |   await expect(page.locator('#purposes .purpose-share-note')).toContainText('Shares use positive group totals');
  108 | });
  109 | 
  110 | test('refund-only groups keep the signed table without a misleading share or empty Sankey', async ({ page }) => {
  111 |   await mock(page, async route => {
  112 |     const data = flow('Card payments & reimbursements');
  113 |     data.totalAmount = -20;
  114 |     data.groups[0].amount = -20;
  115 |     data.links[0].amount = -20;
  116 |     await respond(route, data);
  117 |   });
  118 |   await page.goto('/vendors/purpose-lifecycle/');
  119 |   await expect(page.locator('#purposes tbody tr')).toHaveCount(1);
  120 |   await expect(page.locator('#purposes tbody td').nth(1)).toHaveText('-$20.00');
  121 |   await expect(page.locator('#purposes tbody td').nth(2)).toHaveText('—');
  122 |   await expect(page.locator('#purposes .mf-toggle')).toHaveCount(0);
  123 |   await expect(page.locator('#overview')).toContainText('No positive spending');
  124 | });
  125 | 
```