# 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 >> mobile table scrolls all numeric columns and expands raw purposes at full width
- Location: tests/ui/vendor-purposes.spec.ts:461:5

# 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
  364 |     await installMocks(page);
  365 | 
  366 |     await page.goto(`/vendors/${VENDOR_ID}/`);
  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}/`);
      |              ^ Error: page.goto: net::ERR_ABORTED; maybe frame was detached?
  465 |   await expect(page.locator('#purposes canvas')).toBeVisible();
  466 |   await page.locator('#purposes [data-view="table"]').click();
  467 |   const region = page.getByRole('region', { name: 'Purpose breakdown table' });
  468 |   expect(await region.evaluate(el => el.scrollWidth > el.clientWidth)).toBe(true);
  469 |   await expect(page.getByText('Scroll horizontally to see all columns →')).toBeVisible();
  470 |   expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
  471 |   await page.locator('#purposes tbody summary').first().click();
  472 |   const expanded = page.locator('#purposes .purpose-raw-row td');
  473 |   await expect(expanded).toHaveAttribute('colspan', '5');
  474 |   await expect(expanded).toContainText('MEDIA BUY');
  475 |   expect(await expanded.evaluate(el => el.clientWidth)).toBeGreaterThan(600);
  476 |   await region.evaluate(el => { el.scrollLeft = el.scrollWidth; });
  477 |   const clients = page.locator('#purposes thead th[data-sort="clients"]');
  478 |   const visible = await clients.evaluate(el => {
  479 |     const cell = el.getBoundingClientRect();
  480 |     const viewport = el.closest('.purpose-table-scroll')!.getBoundingClientRect();
  481 |     return cell.right <= viewport.right + 1 && cell.left >= viewport.left;
  482 |   });
  483 |   expect(visible).toBe(true);
  484 | });
  485 | 
```