# 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: canvas.spec.ts >> AI chat user interactions >> whitespace-only prompts cannot be sent
- Location: e2e/canvas.spec.ts:424:7

# Error details

```
Error: expect(locator).toBeDisabled() failed

Locator:  getByRole('button', { name: 'Send message', exact: true })
Expected: disabled
Received: enabled
Timeout:  10000ms

Call log:
  - Expect "toBeDisabled" getByRole('button', { name: 'Send message', exact: true }) with timeout 10000ms
  - waiting for getByRole('button', { name: 'Send message', exact: true })
    23 × locator resolved to <button type="submit" aria-label="Send message" data-onboarding-coach="send" class="inline-flex select-none items-center justify-center gap-2 whitespace-nowrap text-sm font-bold focus-visible:outline-hidden focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 cursor-pointer shadow-sm bg-primary-button border-4 border-transparent hover:border-primary-button-border text-primary-button-text h-8 w-8 transiti…>…</button>
       - unexpected value "enabled"

```

```yaml
- button "Send message"
```

# Test source

```ts
  329 |     await expect(page.getByText("Invite was sent", { exact: true })).toBeVisible();
  330 |     await expect(email).toHaveValue("");
  331 |     await expect(page.getByText("colleague@example.com", { exact: true })).toBeVisible();
  332 |     expect(
  333 |       (await backend.requests()).filter((r) => r.method === "POST" && r.path === "/board/1/share")
  334 |     ).toHaveLength(1);
  335 |   });
  336 | 
  337 |   test("rejected invitations retain the email for correction and retry", async ({
  338 |     page,
  339 |     backend,
  340 |   }) => {
  341 |     await openBoard(page);
  342 |     await page.getByRole("button", { name: "Share board", exact: true }).click();
  343 |     await page.getByRole("button", { name: "People with access", exact: true }).click();
  344 |     const email = page.getByPlaceholder("Enter email address");
  345 |     await email.fill("colleague@example.com");
  346 |     await backend.failNext("POST", "/board/1/share", 400, {
  347 |       message: "That colleague cannot be invited",
  348 |     });
  349 |     await email.press("Enter");
  350 |     await expect(page.getByText("That colleague cannot be invited", { exact: true })).toBeVisible();
  351 |     await expect(email).toHaveValue("colleague@example.com");
  352 |     await email.fill("editor@example.com");
  353 |     await email.press("Enter");
  354 |     await expect(page.getByText("Invite was sent", { exact: true })).toBeVisible();
  355 |     await expect(email).toHaveValue("");
  356 |   });
  357 | });
  358 | 
  359 | const chatNode = {
  360 |   id: "21",
  361 |   name: "AI Assistant",
  362 |   boardId: 1,
  363 |   positionX: 0,
  364 |   positionY: 0,
  365 |   styles: JSON.stringify({ width: 1200, height: 800 }),
  366 |   updateId: 0,
  367 |   chats: [],
  368 | };
  369 | 
  370 | test.describe("AI chat user interactions", () => {
  371 |   test("creates one assistant node and restores it after reload", async ({ page, backend }) => {
  372 |     await openBoard(page);
  373 |     await page
  374 |       .locator(".flow-toolbar")
  375 |       .getByRole("button", { name: "AI Assistant", exact: true })
  376 |       .click();
  377 |     await expect(page.locator(".react-flow__node-chat")).toHaveCount(1);
  378 |     await expect(page.getByRole("textbox", { name: "Write your prompt here" })).toBeVisible();
  379 |     await expect
  380 |       .poll(
  381 |         async () =>
  382 |           (await backend.requests()).filter((r) => r.method === "POST" && r.path === "/chat-node/1")
  383 |             .length
  384 |       )
  385 |       .toBe(1);
  386 |     await page.reload();
  387 |     await expect(page.locator(".react-flow__node-chat")).toHaveCount(1);
  388 |     await expect(page.getByRole("textbox", { name: "Write your prompt here" })).toBeVisible();
  389 |   });
  390 | 
  391 |   test("failed assistant creation removes its optimistic placeholder", async ({
  392 |     page,
  393 |     backend,
  394 |   }) => {
  395 |     await openBoard(page);
  396 |     await backend.failNext("POST", "/chat-node/1", 500);
  397 |     await page
  398 |       .locator(".flow-toolbar")
  399 |       .getByRole("button", { name: "AI Assistant", exact: true })
  400 |       .click();
  401 |     await expect
  402 |       .poll(async () =>
  403 |         (await backend.requests()).some((r) => r.method === "POST" && r.path === "/chat-node/1")
  404 |       )
  405 |       .toBeTruthy();
  406 |     await expect(page.locator(".react-flow__node-chat")).toHaveCount(0);
  407 |   });
  408 | 
  409 |   test("unsent chat drafts survive reload without creating a conversation", async ({
  410 |     page,
  411 |     backend,
  412 |   }) => {
  413 |     await backend.configure({ chatNodes: [chatNode] });
  414 |     await openBoard(page);
  415 |     const composer = page.getByRole("textbox", { name: "Write your prompt here" });
  416 |     await composer.fill("Remember this unsent idea");
  417 |     await page.reload();
  418 |     await expect(composer).toHaveValue("Remember this unsent idea");
  419 |     expect(
  420 |       (await backend.requests()).filter((r) => r.method === "POST" && r.path === "/chat")
  421 |     ).toHaveLength(0);
  422 |   });
  423 | 
  424 |   test("whitespace-only prompts cannot be sent", async ({ page, backend }) => {
  425 |     await backend.configure({ chatNodes: [chatNode] });
  426 |     await openBoard(page);
  427 |     const composer = page.getByRole("textbox", { name: "Write your prompt here" });
  428 |     await composer.fill("   ");
> 429 |     await expect(page.getByRole("button", { name: "Send message", exact: true })).toBeDisabled();
      |                                                                                   ^ Error: expect(locator).toBeDisabled() failed
  430 |     await composer.press("Enter");
  431 |     await expect(composer).toHaveValue("   ");
  432 |     expect(
  433 |       (await backend.requests()).filter((r) => r.method === "POST" && r.path === "/chat")
  434 |     ).toHaveLength(0);
  435 |   });
  436 | 
  437 |   test("Shift+Enter inserts a line break while Delete edits the prompt safely", async ({
  438 |     page,
  439 |     backend,
  440 |   }) => {
  441 |     await backend.configure({ chatNodes: [chatNode] });
  442 |     await openBoard(page);
  443 |     const composer = page.getByRole("textbox", { name: "Write your prompt here" });
  444 |     await composer.fill("First line");
  445 |     await composer.press("End");
  446 |     await composer.press("Shift+Enter");
  447 |     await composer.pressSequentially("Second line");
  448 |     await expect(composer).toHaveValue("First line\nSecond line");
  449 |     await composer.press("Backspace");
  450 |     await expect(composer).toHaveValue("First line\nSecond lin");
  451 |     await expect(page.locator(".react-flow__node-chat")).toHaveCount(1);
  452 |     expect(
  453 |       (await backend.requests()).filter((r) => r.method === "POST" && r.path === "/chat")
  454 |     ).toHaveLength(0);
  455 |   });
  456 | 
  457 |   test("sending a prompt renders the streamed assistant response and unlocks input", async ({
  458 |     page,
  459 |     backend,
  460 |   }) => {
  461 |     await backend.configure({ chatNodes: [chatNode] });
  462 |     await openBoard(page);
  463 |     const composer = page.getByRole("textbox", { name: "Write your prompt here" });
  464 |     await composer.fill("Summarize my audience research");
  465 |     await composer.press("Enter");
  466 |     await expect(page.getByText("Summarize my audience research", { exact: true })).toBeVisible();
  467 |     await expect(
  468 |       page.getByText("Here is a focused summary of your research.", { exact: true })
  469 |     ).toBeVisible();
  470 |     await expect(composer).toHaveValue("");
  471 |     await expect(page.getByRole("button", { name: "Stop generating", exact: true })).toHaveCount(0);
  472 |     await expect
  473 |       .poll(
  474 |         async () =>
  475 |           (await backend.requests()).filter((r) => r.method === "POST" && r.path.endsWith("/save"))
  476 |             .length
  477 |       )
  478 |       .toBe(1);
  479 |     await page.reload();
  480 |     await expect(page.getByText("Summarize my audience research", { exact: true })).toBeVisible();
  481 |     await expect(
  482 |       page.getByText("Here is a focused summary of your research.", { exact: true })
  483 |     ).toBeVisible();
  484 |   });
  485 | });
  486 | 
```