# 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: sources.spec.ts >> Board access >> viewer cannot import files by dropping them onto a read-only board
- Location: e2e/sources.spec.ts:236:7

# Error details

```
Error: expect(received).toHaveLength(expected)

Expected length: 0
Received length: 1
Received array:  [{"body": {"boardId": 3, "positionX": 1400, "positionY": 950, "styles": "{\"width\":500,\"height\":400,\"mode\":\"full\"}", "text": "<p>This must remain read-only</p>"}, "method": "POST", "path": "/source/text", "query": {}}]
```

# Page snapshot

```yaml
- generic [active] [ref=f2e1]:
  - main [ref=f2e3]:
    - application [ref=f2e7]:
      - group [ref=f2e10]:
        - generic [ref=f2e11]:
          - generic [ref=f2e13] [cursor=pointer]:
            - generic [ref=f2e16]: Text
            - generic [ref=f2e17]:
              - button [ref=f2e18]
              - button "Open menu" [ref=f2e20]
          - generic [ref=f2e23]:
            - paragraph [ref=f2e29]: This must remain read-only
            - button [disabled] [ref=f2e32]
          - generic [ref=f2e36] [cursor=pointer]
          - generic [ref=f2e37] [cursor=pointer]
      - generic "Control Panel" [ref=f2e38]:
        - generic [ref=f2e39]:
          - generic "Zoom in" [ref=f2e40]:
            - button "Zoom in" [ref=f2e41] [cursor=pointer]
          - generic "Zoom out" [ref=f2e43]:
            - button "Zoom out" [ref=f2e44] [cursor=pointer]
          - generic "Fit view" [ref=f2e46]:
            - button "Fit view" [ref=f2e47] [cursor=pointer]
      - navigation [ref=f2e53]:
        - generic [ref=f2e55]:
          - button "Logo Blort" [ref=f2e56] [cursor=pointer]:
            - generic [ref=f2e57]:
              - img "Logo" [ref=f2e59]
              - generic [ref=f2e60]: Blort
          - generic [ref=f2e63]: Shared inspiration
      - navigation:
        - generic [ref=f2e65]:
          - generic [ref=f2e68]:
            - generic [ref=f2e69]: Template Board - View Only
            - button "Use Template" [ref=f2e70] [cursor=pointer]
          - button "Keyboard shortcuts" [ref=f2e71] [cursor=pointer]:
            - img "Keyboard shortcuts"
          - button "Toggle visual analysis" [pressed] [ref=f2e72] [cursor=pointer]
      - navigation:
        - generic [ref=f2e75]:
          - generic "AI Assistant" [ref=f2e76]:
            - button "AI Assistant" [disabled]:
              - generic:
                - img "AI Assistant"
          - generic "Create group" [ref=f2e78]:
            - button "Create group" [disabled]:
              - generic:
                - img "Create group"
          - generic "Add Text" [ref=f2e79]:
            - button "Add Text" [disabled]:
              - generic:
                - img "Add Text"
          - generic "Annotation" [ref=f2e80]:
            - button "Annotation" [disabled]:
              - generic:
                - img "Annotation"
          - generic "Sticky Note" [ref=f2e81]:
            - button "Sticky Note" [disabled]:
              - generic:
                - img "Sticky Note"
          - generic "Draw" [ref=f2e83]:
            - button "Draw" [disabled]:
              - generic:
                - img "Draw"
          - generic "Shapes" [ref=f2e85]:
            - button "Shapes" [disabled]:
              - generic:
                - img "Shapes"
          - generic "Add Content" [ref=f2e87]:
            - button "Add Content" [disabled]:
              - generic:
                - img "Add Content"
          - generic "Extract Videos" [ref=f2e88]:
            - button "Extract Videos" [disabled]:
              - generic:
                - img "Extract Videos"
          - generic "Meta Ads" [ref=f2e89]:
            - button "Meta Ads" [disabled]:
              - generic:
                - img "Meta Ads"
          - generic "Add File" [ref=f2e90]:
            - button "Add File" [disabled]:
              - generic:
                - img "Add File"
          - generic "Add Website" [ref=f2e91]:
            - button "Add Website" [disabled]:
              - generic:
                - img "Add Website"
          - generic "Search Videos" [ref=f2e92]:
            - button "Search Videos" [disabled]:
              - generic:
                - img "Search Videos"
      - generic:
        - button [aria-hidden]
  - region "Notifications (F8)":
    - list
  - button "Open Next.js Dev Tools" [ref=f2e99] [cursor=pointer]
  - alert [ref=f2e103]
```

# Test source

```ts
  162 |     const transfer = await page.evaluateHandle(() => {
  163 |       const data = new DataTransfer();
  164 |       data.items.add(
  165 |         new File(["An imported research observation"], "research.txt", { type: "text/plain" })
  166 |       );
  167 |       return data;
  168 |     });
  169 |     await page
  170 |       .locator(".react-flow__pane")
  171 |       .dispatchEvent("drop", { dataTransfer: transfer, clientX: 650, clientY: 450 });
  172 |     await settleFileDrop(page, transfer);
  173 |     await expect(page.locator(".react-flow__node-text")).toContainText(
  174 |       "An imported research observation"
  175 |     );
  176 |     await expect
  177 |       .poll(async () =>
  178 |         (await backend.requests()).some((r) => r.method === "POST" && r.path === "/source/text")
  179 |       )
  180 |       .toBeTruthy();
  181 |     await page.reload();
  182 |     await expect(page.locator(".react-flow__node-text")).toContainText(
  183 |       "An imported research observation"
  184 |     );
  185 |   });
  186 |   test("oversized dropped text files are rejected before any source creation", async ({
  187 |     page,
  188 |     backend,
  189 |   }) => {
  190 |     const transfer = await page.evaluateHandle(() => {
  191 |       const data = new DataTransfer();
  192 |       data.items.add(
  193 |         new File(["x".repeat(1024 * 1024 + 1)], "oversized.txt", { type: "text/plain" })
  194 |       );
  195 |       return data;
  196 |     });
  197 |     await page
  198 |       .locator(".react-flow__pane")
  199 |       .dispatchEvent("drop", { dataTransfer: transfer, clientX: 650, clientY: 450 });
  200 |     await settleFileDrop(page, transfer);
  201 |     await expect(page.getByText("File too large", { exact: true })).toBeVisible();
  202 |     await expect(page.locator(".react-flow__node-text")).toHaveCount(0);
  203 |     expect(
  204 |       (await backend.requests()).filter((r) => r.method === "POST" && r.path === "/source/text")
  205 |     ).toHaveLength(0);
  206 |   });
  207 | });
  208 | 
  209 | test.describe("Board access", () => {
  210 |   test("viewer can read content but toolbar and keyboard cannot mutate it", async ({
  211 |     page,
  212 |     backend,
  213 |   }) => {
  214 |     await page.goto("/board/3");
  215 |     await expect(page.locator(".react-flow")).toBeVisible();
  216 |     for (const name of ["Add Text", "Sticky Note", "AI Assistant", "Add Website", "Create group"]) {
  217 |       await expect(
  218 |         page.locator(".flow-toolbar").getByRole("button", { name, exact: true })
  219 |       ).toBeDisabled();
  220 |     }
  221 |     await expect(page.getByRole("button", { name: "Share board", exact: true })).toHaveCount(0);
  222 |     await page.locator(".react-flow__pane").click({ position: { x: 1100, y: 500 } });
  223 |     await page.keyboard.press("t");
  224 |     await page.keyboard.press("Control+a");
  225 |     await page.keyboard.press("Delete");
  226 |     expect(
  227 |       (await backend.requests()).filter(
  228 |         (r) =>
  229 |           (r.method === "POST" &&
  230 |             ["/source/text", "/chat-node/3", "/board/3/items/batch/delete"].includes(r.path)) ||
  231 |           r.method === "DELETE"
  232 |       )
  233 |     ).toHaveLength(0);
  234 |   });
  235 | 
  236 |   test("viewer cannot import files by dropping them onto a read-only board", async ({
  237 |     page,
  238 |     backend,
  239 |   }) => {
  240 |     await page.goto("/board/3");
  241 |     await expect(
  242 |       page.locator(".flow-toolbar").getByRole("button", { name: "Add Text", exact: true })
  243 |     ).toBeDisabled();
  244 |     const transfer = await page.evaluateHandle(() => {
  245 |       const data = new DataTransfer();
  246 |       data.items.add(
  247 |         new File(["This must remain read-only"], "viewer.txt", { type: "text/plain" })
  248 |       );
  249 |       return data;
  250 |     });
  251 |     await page
  252 |       .locator(".react-flow__pane")
  253 |       .dispatchEvent("drop", { dataTransfer: transfer, clientX: 650, clientY: 450 });
  254 |     await settleFileDrop(page, transfer);
  255 |     await expect(page.locator(".react-flow__node-text")).toHaveCount(0);
  256 |     await page.reload();
  257 |     await expect(
  258 |       page.locator(".flow-toolbar").getByRole("button", { name: "Add Text", exact: true })
  259 |     ).toBeDisabled();
  260 |     expect(
  261 |       (await backend.requests()).filter((r) => r.method === "POST" && r.path === "/source/text")
> 262 |     ).toHaveLength(0);
      |       ^ Error: expect(received).toHaveLength(expected)
  263 |   });
  264 | 
  265 |   test("preview boards cannot import files through drag and drop", async ({ page, backend }) => {
  266 |     await page.goto("/board/1?preview=1");
  267 |     await expect(page.locator(".react-flow")).toBeVisible();
  268 |     await expect(
  269 |       page.locator(".flow-toolbar").getByRole("button", { name: "Add Text", exact: true })
  270 |     ).toHaveCount(0);
  271 |     const transfer = await page.evaluateHandle(() => {
  272 |       const data = new DataTransfer();
  273 |       data.items.add(
  274 |         new File(["This preview must remain unchanged"], "preview.txt", { type: "text/plain" })
  275 |       );
  276 |       return data;
  277 |     });
  278 |     await page
  279 |       .locator(".react-flow__pane")
  280 |       .dispatchEvent("drop", { dataTransfer: transfer, clientX: 650, clientY: 450 });
  281 |     await settleFileDrop(page, transfer);
  282 |     await expect(page.locator(".react-flow__node-text")).toHaveCount(0);
  283 |     await page.reload();
  284 |     await expect(page.locator(".react-flow")).toBeVisible();
  285 |     expect(
  286 |       (await backend.requests()).filter((r) => r.method === "POST" && r.path === "/source/text")
  287 |     ).toHaveLength(0);
  288 |   });
  289 | 
  290 |   test("preview text content is readable but cannot be edited", async ({ page, backend }) => {
  291 |     await backend.configure({
  292 |       sources: [
  293 |         {
  294 |           id: 11,
  295 |           boardId: 1,
  296 |           sourceId: "11",
  297 |           positionX: 0,
  298 |           positionY: 0,
  299 |           styles: JSON.stringify({ width: 600, height: 400, mode: "full" }),
  300 |           updateId: 0,
  301 |           source: {
  302 |             id: 11,
  303 |             type: "text",
  304 |             status: "success",
  305 |             title: "Preview research",
  306 |             text: "<p>A read-only preview observation</p>",
  307 |           },
  308 |         },
  309 |       ],
  310 |     });
  311 |     await page.goto("/board/1?preview=1");
  312 |     const node = page.locator('.react-flow__node[data-id="source-11"]');
  313 |     await expect(node).toContainText("A read-only preview observation");
  314 |     await expect(node.locator(".tiptap")).toHaveAttribute("contenteditable", "false");
  315 |     expect(
  316 |       (await backend.requests()).filter((r) => r.method === "PATCH" && r.path === "/source/11/text")
  317 |     ).toHaveLength(0);
  318 |   });
  319 | 
  320 |   test("missing board shows an actionable access error instead of an endless spinner", async ({
  321 |     page,
  322 |   }) => {
  323 |     await page.goto("/board/99999");
  324 |     await expect(page.getByText("Failed to load board", { exact: false })).toBeVisible();
  325 |     await expect(page.getByRole("button", { name: "Go to home", exact: false })).toBeVisible();
  326 |     await expect(page.locator(".react-flow")).toHaveCount(0);
  327 |   });
  328 | });
  329 | 
```