# 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 — Sankey / Table toggle >> Table button shows the table and hides the chart
- Location: tests/ui/vendor-purposes.spec.ts:363: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
  266 |     await expect(page.locator('#purposes tbody tr')).toHaveCount(3, { timeout: DATA_LOAD_TIMEOUT });
  267 | 
  268 |     await expect(page.locator('#purposes tbody tr').first()).toContainText('Media Buying');
  269 |     await expect(page.locator('#purposes thead th[data-sort="amount"]')).toHaveAttribute('aria-sort', 'descending');
  270 |   });
  271 | 
  272 |   test('clicking Group sorts alphabetically ascending, then descending', async ({ page }) => {
  273 |     await installMocks(page);
  274 | 
  275 |     await page.goto(`/vendors/${VENDOR_ID}/`);
  276 |     await expect(page.locator('#purposes tbody tr')).toHaveCount(3, { timeout: DATA_LOAD_TIMEOUT });
  277 | 
  278 |     await page.locator('#purposes thead th[data-sort="group"] button').click();
  279 |     const rows = page.locator('#purposes tbody tr');
  280 |     await expect(rows.first()).toContainText('Consulting');
  281 |     await expect(page.locator('#purposes thead th[data-sort="group"]')).toHaveAttribute('aria-sort', 'ascending');
  282 | 
  283 |     await page.locator('#purposes thead th[data-sort="group"] button').click();
  284 |     await expect(rows.first()).toContainText('Media Buying');
  285 |     await expect(page.locator('#purposes thead th[data-sort="group"]')).toHaveAttribute('aria-sort', 'descending');
  286 |   });
  287 | 
  288 |   test('clicking the active Amount header flips from desc to asc and back', async ({ page }) => {
  289 |     await installMocks(page);
  290 | 
  291 |     await page.goto(`/vendors/${VENDOR_ID}/`);
  292 |     await expect(page.locator('#purposes tbody tr')).toHaveCount(3, { timeout: DATA_LOAD_TIMEOUT });
  293 | 
  294 |     // amount is active desc; clicking it (same key, desc → asc)
  295 |     await page.locator('#purposes thead th[data-sort="amount"] button').click();
  296 |     await expect(page.locator('#purposes thead th[data-sort="amount"]')).toHaveAttribute('aria-sort', 'ascending');
  297 |     // asc: Consulting (1M) first
  298 |     await expect(page.locator('#purposes tbody tr').first()).toContainText('Consulting');
  299 | 
  300 |     // click again → back to desc: Media Buying (6M) first
  301 |     await page.locator('#purposes thead th[data-sort="amount"] button').click();
  302 |     await expect(page.locator('#purposes thead th[data-sort="amount"]')).toHaveAttribute('aria-sort', 'descending');
  303 |     await expect(page.locator('#purposes tbody tr').first()).toContainText('Media Buying');
  304 |   });
  305 | 
  306 |   test('non-active headers show aria-sort="none"', async ({ page }) => {
  307 |     await installMocks(page);
  308 | 
  309 |     await page.goto(`/vendors/${VENDOR_ID}/`);
  310 |     await expect(page.locator('#purposes thead')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  311 | 
  312 |     // amount is the active sort; group, share, count, clients are none
  313 |     await expect(page.locator('#purposes thead th[data-sort="group"]')).toHaveAttribute('aria-sort', 'none');
  314 |     await expect(page.locator('#purposes thead th[data-sort="share"]')).toHaveAttribute('aria-sort', 'none');
  315 |   });
  316 | });
  317 | 
  318 | test.describe('vendor purpose card — expandable group detail', () => {
  319 |   test.beforeEach(async ({ page }) => {
  320 |     await page.addInitScript(() => localStorage.setItem('electinfo:money-flow-view', 'table'));
  321 |   });
  322 |   test('each row has a details/summary element with the group name', async ({ page }) => {
  323 |     await installMocks(page);
  324 | 
  325 |     await page.goto(`/vendors/${VENDOR_ID}/`);
  326 |     await expect(page.locator('#purposes tbody tr')).toHaveCount(3, { timeout: DATA_LOAD_TIMEOUT });
  327 | 
  328 |     const rows = page.locator('#purposes tbody tr');
  329 |     for (let i = 0; i < 3; i++) {
  330 |       await expect(rows.nth(i).locator('details')).toHaveCount(1);
  331 |     }
  332 |     // first row by default sort is Media Buying
  333 |     await expect(rows.first().locator('details summary')).toContainText('Media Buying');
  334 |   });
  335 | 
  336 |   test('expanding details reveals topPurposes for that group', async ({ page }) => {
  337 |     await installMocks(page);
  338 | 
  339 |     await page.goto(`/vendors/${VENDOR_ID}/`);
  340 |     await expect(page.locator('#purposes tbody tr')).toHaveCount(3, { timeout: DATA_LOAD_TIMEOUT });
  341 | 
  342 |     const firstDetails = page.locator('#purposes tbody tr').first().locator('details');
  343 |     await firstDetails.locator('summary').click();
  344 |     await expect(firstDetails).toHaveAttribute('open', '');
  345 |     await expect(page.locator('#purposes .purpose-raw-row')).toContainText('MEDIA BUY');
  346 |     await expect(page.locator('#purposes .purpose-raw-row')).toContainText('AD PLACEMENT');
  347 |   });
  348 | });
  349 | 
  350 | test.describe('vendor purpose card — Sankey / Table toggle', () => {
  351 |   test('Sankey is the default view with a canvas visible', async ({ page }) => {
  352 |     const errors: string[] = [];
  353 |     page.on('pageerror', e => errors.push(String(e)));
  354 |     await installMocks(page);
  355 | 
  356 |     await page.goto(`/vendors/${VENDOR_ID}/`);
  357 |     await expect(page.locator('#purposes .money-flow-chart canvas')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  358 |     await expect(page.locator('#purposes .mf-view-btn[data-view="sankey"]')).toHaveAttribute('aria-pressed', 'true');
  359 |     await expect(page.locator('#purposes .purpose-table-view')).toBeHidden();
  360 |     expect(errors).toEqual([]);
  361 |   });
  362 | 
  363 |   test('Table button shows the table and hides the chart', async ({ page }) => {
  364 |     await installMocks(page);
  365 | 
> 366 |     await page.goto(`/vendors/${VENDOR_ID}/`);
      |                ^ Error: page.goto: net::ERR_CONNECTION_REFUSED at http://10.1.105.22/vendors/vendor-purpose-test/
  367 |     await expect(page.locator('#purposes .money-flow-chart canvas')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  368 | 
  369 |     await page.locator('#purposes .mf-view-btn[data-view="table"]').click();
  370 |     await expect(page.locator('#purposes .purpose-table-view')).toBeVisible();
  371 |     await expect(page.locator('#purposes .money-flow-chart')).toBeHidden();
  372 |     await expect(page.locator('#purposes .mf-view-btn[data-view="table"]')).toHaveAttribute('aria-pressed', 'true');
  373 |   });
  374 | 
  375 |   test('switching back to Sankey restores the canvas', async ({ page }) => {
  376 |     await installMocks(page);
  377 | 
  378 |     await page.goto(`/vendors/${VENDOR_ID}/`);
  379 |     await expect(page.locator('#purposes .money-flow-chart canvas')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  380 |     await page.locator('#purposes .mf-view-btn[data-view="table"]').click();
  381 |     await page.locator('#purposes .mf-view-btn[data-view="sankey"]').click();
  382 |     await expect(page.locator('#purposes .money-flow-chart canvas')).toBeVisible();
  383 |     await expect(page.locator('#purposes .purpose-table-view')).toBeHidden();
  384 |     await expect(page.locator('#purposes .mf-view-btn[data-view="sankey"]')).toHaveAttribute('aria-pressed', 'true');
  385 |   });
  386 | });
  387 | 
  388 | test.describe('vendor purpose card — fallback / empty states', () => {
  389 |   test('"Raw purposes" disclosure appears when vendorPurposeFlow returns null', async ({ page }) => {
  390 |     await installMocks(page, null);
  391 | 
  392 |     await page.goto(`/vendors/${VENDOR_ID}/`);
  393 |     await expect(page.locator('#purposes')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  394 | 
  395 |     const disclosure = page.locator('#purposes details.purpose-raw');
  396 |     await expect(disclosure).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  397 |     await expect(disclosure.locator('summary')).toHaveText('Raw purposes');
  398 |   });
  399 | 
  400 |   test('"Raw purposes" disclosure appears when groups array is empty', async ({ page }) => {
  401 |     await installMocks(page, { taxonomyVersion: null, totalAmount: 0, groups: [], links: [], otherLinks: [] });
  402 | 
  403 |     await page.goto(`/vendors/${VENDOR_ID}/`);
  404 |     await expect(page.locator('#purposes')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  405 |     await expect(page.locator('#purposes details.purpose-raw')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  406 |   });
  407 | 
  408 |   test('no "Raw purposes" disclosure when valid groups are present', async ({ page }) => {
  409 |     await installMocks(page);
  410 | 
  411 |     await page.goto(`/vendors/${VENDOR_ID}/`);
  412 |     await expect(page.locator('#purposes tbody tr')).toHaveCount(3, { timeout: DATA_LOAD_TIMEOUT });
  413 |     await expect(page.locator('#purposes details.purpose-raw')).toHaveCount(0);
  414 |   });
  415 | 
  416 |   test('status element shows a message while loading and clears on success', async ({ page }) => {
  417 |     await installMocks(page);
  418 | 
  419 |     await page.goto(`/vendors/${VENDOR_ID}/`);
  420 |     const select = page.locator('#purposes select[aria-label="Purpose cycle"]');
  421 |     await expect(select).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
  422 | 
  423 |     await select.selectOption('2022');
  424 |     // status clears once the new data is rendered
  425 |     await expect(page.locator('#purposes [role="status"]')).toHaveText('', { timeout: DATA_LOAD_TIMEOUT });
  426 |   });
  427 | });
  428 | 
  429 | test('three-column Sankey fills the card and contains both link stages after mount and resize', async ({ page }) => {
  430 |   await installMocks(page);
  431 |   await page.goto(`/vendors/${VENDOR_ID}/`);
  432 |   await expect(page.locator('#purposes canvas')).toBeVisible();
  433 |   const inspect = () => page.locator('#purposes .money-flow-chart').evaluate(el => {
  434 |     const diagnostics = window.electinfoMoneyFlow!.inspect!(el.parentElement!)!;
  435 |     const option = diagnostics.option as any;
  436 |     return { width: diagnostics.width, cardWidth: el.closest('section')!.getBoundingClientRect().width,
  437 |       containerWidth: el.clientWidth, animation: option.animation,
  438 |       nodes: option.series[0].data.map((n: any) => ({ id: n.name, depth: n.depth, label: n.raw.name })),
  439 |       links: option.series[0].links.map((l: any) => ({ source: l.source, target: l.target })) };
  440 |   });
  441 |   const chart = await inspect();
  442 |   expect(chart.width).toBeGreaterThanOrEqual(chart.cardWidth * .9);
  443 |   expect(chart.animation).toBe(false);
  444 |   expect(chart.nodes).toEqual(expect.arrayContaining([
  445 |     expect.objectContaining({ label: 'DEM VICTORY FUND', depth: 0 }),
  446 |     expect.objectContaining({ label: 'Media Buying', depth: 1 }),
  447 |     expect.objectContaining({ label: VENDOR_NAME, depth: 2 }),
  448 |   ]));
  449 |   expect(chart.links).toEqual(expect.arrayContaining([
  450 |     { source: 'in:c-dem', target: 'purpose:Media Buying' },
  451 |     { source: 'purpose:Media Buying', target: 'focal' },
  452 |   ]));
  453 |   await page.setViewportSize({ width: 760, height: 900 });
  454 |   await expect.poll(async () => { const c = await inspect(); return Math.abs(c.width - c.containerWidth); }).toBeLessThan(2);
  455 |   await page.locator('#purposes [data-view="table"]').click();
  456 |   await page.setViewportSize({ width: 1100, height: 900 });
  457 |   await page.locator('#purposes [data-view="sankey"]').click();
  458 |   await expect.poll(async () => { const c = await inspect(); return Math.abs(c.width - c.containerWidth); }).toBeLessThan(2);
  459 | });
  460 | 
  461 | test('mobile table scrolls all numeric columns and expands raw purposes at full width', async ({ page }) => {
  462 |   await page.setViewportSize({ width: 390, height: 844 });
  463 |   await installMocks(page);
  464 |   await page.goto(`/vendors/${VENDOR_ID}/`);
  465 |   await expect(page.locator('#purposes canvas')).toBeVisible();
  466 |   await page.locator('#purposes [data-view="table"]').click();
```