From 84ece21066e2f3ba8f42f1ae57aaebefe625eac1 Mon Sep 17 00:00:00 2001 From: Laurent Paoletti Date: Thu, 12 Mar 2026 16:25:46 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=85(e2e)=20add=20dedicated=20e2e=20tests?= =?UTF-8?q?=20for=20file=20paste=20in=20chat=20input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Laurent Paoletti --- CHANGELOG.md | 1 + .../__tests__/app-conversations/chat.spec.ts | 71 ----- .../app-conversations/file-paste.spec.ts | 270 ++++++++++++++++++ 3 files changed, 271 insertions(+), 71 deletions(-) create mode 100644 src/frontend/apps/e2e/__tests__/app-conversations/file-paste.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a2350ddd..651ef4c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to - ✨(back) add projects with custom LLM instructions - ✨(front) projects management UI +- ✅(e2e) add dedicated e2e tests for file paste in chat input ### Changed diff --git a/src/frontend/apps/e2e/__tests__/app-conversations/chat.spec.ts b/src/frontend/apps/e2e/__tests__/app-conversations/chat.spec.ts index ddb1bfd4..8b780df0 100644 --- a/src/frontend/apps/e2e/__tests__/app-conversations/chat.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-conversations/chat.spec.ts @@ -1,7 +1,5 @@ import { expect, test } from '@playwright/test'; -import { overrideConfig } from './common'; - test.beforeEach(async ({ page }) => { await page.goto('/home/'); }); @@ -71,73 +69,4 @@ test.describe('Chat page', () => { await chatHistoryLink.click(); await expect(messageContent).toBeVisible(); }); - - test('the user can paste a document into the chat input', async ({ - page, - }) => { - await overrideConfig(page, { - FEATURE_FLAGS: { - 'document-upload': 'enabled', - 'web-search': 'enabled', - }, - }); - - await page.goto('/'); - - const chatInput = page.getByRole('textbox', { - name: 'Enter your message or a', - }); - await expect(chatInput).toBeVisible(); - await chatInput.click(); - - // Create a test file content - const fileContent = 'Test document content for paste'; - const fileName = 'test-document.txt'; - const fileType = 'text/plain'; - - // Simulate paste event with file - await page.evaluate( - ({ content, name, type }) => { - const textarea = document.querySelector( - 'textarea[name="inputchat-textarea"]', - ) as HTMLTextAreaElement; - if (!textarea) return; - - // Create a File object - const file = new File([content], name, { type }); - - // Create a DataTransfer object to simulate clipboard - const dataTransfer = new DataTransfer(); - dataTransfer.items.add(file); - - // Create a paste event - ClipboardEvent constructor doesn't accept clipboardData - // so we create a regular Event and add clipboardData property - const pasteEvent = new Event('paste', { - bubbles: true, - cancelable: true, - }) as unknown as ClipboardEvent; - - // Define clipboardData property to make it accessible - Object.defineProperty(pasteEvent, 'clipboardData', { - value: { - files: dataTransfer.files, - items: dataTransfer.items, - types: Array.from(dataTransfer.types), - getData: () => '', - setData: () => {}, - }, - writable: false, - configurable: true, - }); - - textarea.dispatchEvent(pasteEvent); - }, - { content: fileContent, name: fileName, type: fileType }, - ); - - // Wait for the file to be processed and appear in the attachment list - // The attachment should be visible with the file name - const attachment = page.getByText(fileName, { exact: false }).first(); - await expect(attachment).toBeVisible({ timeout: 5000 }); - }); }); diff --git a/src/frontend/apps/e2e/__tests__/app-conversations/file-paste.spec.ts b/src/frontend/apps/e2e/__tests__/app-conversations/file-paste.spec.ts new file mode 100644 index 00000000..51389bf6 --- /dev/null +++ b/src/frontend/apps/e2e/__tests__/app-conversations/file-paste.spec.ts @@ -0,0 +1,270 @@ +import { Page, expect, test } from '@playwright/test'; + +import { overrideConfig } from './common'; + +type FileDescriptor = { + content: string; + name: string; + type: string; + lastModified?: number; +}; + +/** + * Simulates pasting one or more files into the chat textarea + * via a single clipboard event. + */ +const pasteFiles = async (page: Page, files: FileDescriptor[]) => { + await page.evaluate((descriptors) => { + const selector = 'textarea[name="inputchat-textarea"]'; + const textarea = document.querySelector(selector) as HTMLTextAreaElement; + if (!textarea) { + throw new Error( + `Chat textarea not found: '${selector}' - selector mismatch or UI change`, + ); + } + + const dataTransfer = new DataTransfer(); + for (const { content, name, type, lastModified } of descriptors) { + const file = new File([content], name, { + type, + ...(lastModified !== undefined && { lastModified }), + }); + dataTransfer.items.add(file); + } + + const pasteEvent = new Event('paste', { + bubbles: true, + cancelable: true, + }) as unknown as ClipboardEvent; + + Object.defineProperty(pasteEvent, 'clipboardData', { + value: { + files: dataTransfer.files, + items: dataTransfer.items, + types: Array.from(dataTransfer.types), + getData: () => '', + setData: () => {}, + }, + writable: false, + configurable: true, + }); + + textarea.dispatchEvent(pasteEvent); + }, files); +}; + +/** Convenience wrapper for pasting a single file. */ +const pasteFile = async (page: Page, file: FileDescriptor) => { + await pasteFiles(page, [file]); +}; + +test.describe('File paste in chat input', () => { + test.beforeEach(async ({ page }) => { + await overrideConfig(page, { + FEATURE_FLAGS: { + 'document-upload': 'enabled', + 'web-search': 'enabled', + }, + }); + + await page.goto('/'); + + const chatInput = page.getByRole('textbox', { + name: 'Enter your message or a', + }); + await expect(chatInput).toBeVisible(); + await chatInput.click(); + }); + + test('the user can paste a document into the chat input', async ({ + page, + }) => { + const fileContent = 'Test document content for paste'; + const fileName = 'test-document.txt'; + const fileType = 'text/plain'; + + await pasteFile(page, { + content: fileContent, + name: fileName, + type: fileType, + }); + + const attachment = page.getByText(fileName, { exact: false }).first(); + await expect(attachment).toBeVisible({ timeout: 5000 }); + }); + + test('pasting a PDF file adds it as an attachment', async ({ page }) => { + await pasteFile(page, { + content: '%PDF-1.4 fake content', + name: 'report.pdf', + type: 'application/pdf', + }); + + await expect(page.getByText('report.pdf')).toBeVisible({ timeout: 5000 }); + }); + + test('pasting an image file adds it as an attachment', async ({ page }) => { + await pasteFile(page, { + content: 'fake-png-data', + name: 'screenshot.png', + type: 'image/png', + }); + + await expect(page.getByText('screenshot.png')).toBeVisible({ + timeout: 5000, + }); + }); + + test('pasting an unsupported file type shows an error toast', async ({ + page, + }) => { + await pasteFile(page, { + content: 'binary data', + name: 'archive.zip', + type: 'application/zip', + }); + + await expect(page.getByText('File type not supported')).toBeVisible({ + timeout: 5000, + }); + + // The file should NOT appear as an attachment + await expect(page.getByText('archive.zip')).toBeHidden(); + }); + + test('pasting a file when upload is disabled does nothing', async ({ + page, + }) => { + // Re-override config with upload disabled + await overrideConfig(page, { + FEATURE_FLAGS: { + 'document-upload': 'disabled', + 'web-search': 'enabled', + }, + }); + await page.goto('/'); + + const chatInput = page.getByRole('textbox', { + name: 'Enter your message or a', + }); + await expect(chatInput).toBeVisible(); + await chatInput.click(); + + await pasteFile(page, { + content: 'Hello world', + name: 'notes.txt', + type: 'text/plain', + }); + + // No attachment should appear + await expect(page.getByText('notes.txt')).toBeHidden(); + await expect( + page.getByRole('button', { name: 'Remove attachment' }), + ).toBeHidden(); + }); + + test('pasting the same file twice does not create a duplicate', async ({ + page, + }) => { + const file = { + content: 'duplicate test', + name: 'duplicate.txt', + type: 'text/plain', + lastModified: 1700000000000, + }; + + await pasteFile(page, file); + await expect(page.getByText('duplicate.txt')).toBeVisible({ + timeout: 5000, + }); + + await pasteFile(page, file); + + const removeButtons = page.getByRole('button', { + name: 'Remove attachment', + }); + await expect(removeButtons).toHaveCount(1); + }); + + test('pasting multiple different files shows all attachments', async ({ + page, + }) => { + await pasteFiles(page, [ + { content: 'text content', name: 'first.txt', type: 'text/plain' }, + { content: '%PDF-1.4 content', name: 'second.pdf', type: 'application/pdf' }, + ]); + + await expect(page.getByText('first.txt')).toBeVisible({ timeout: 5000 }); + await expect(page.getByText('second.pdf')).toBeVisible({ timeout: 5000 }); + + const removeButtons = page.getByRole('button', { + name: 'Remove attachment', + }); + await expect(removeButtons).toHaveCount(2); + }); + + test('removing a pasted attachment works', async ({ page }) => { + await pasteFile(page, { + content: 'to be removed', + name: 'removeme.txt', + type: 'text/plain', + }); + await expect(page.getByText('removeme.txt')).toBeVisible({ + timeout: 5000, + }); + + await page.getByRole('button', { name: 'Remove attachment' }).click(); + + await expect(page.getByText('removeme.txt')).toBeHidden(); + await expect( + page.getByRole('button', { name: 'Remove attachment' }), + ).toBeHidden(); + }); + + test('pasting text (not a file) should not create an attachment', async ({ + page, + }) => { + const chatInput = page.getByRole('textbox', { + name: 'Enter your message or a', + }); + + await page.evaluate(() => { + const selector = 'textarea[name="inputchat-textarea"]'; + const textarea = document.querySelector( + selector, + ) as HTMLTextAreaElement; + if (!textarea) { + throw new Error( + `Chat textarea not found: '${selector}' - selector mismatch or UI change`, + ); + } + + const pasteEvent = new Event('paste', { + bubbles: true, + cancelable: true, + }) as unknown as ClipboardEvent; + + Object.defineProperty(pasteEvent, 'clipboardData', { + value: { + files: new DataTransfer().files, + items: [], + types: ['text/plain'], + getData: () => 'just plain text', + setData: () => {}, + }, + writable: false, + configurable: true, + }); + + textarea.dispatchEvent(pasteEvent); + }); + + // No attachment should be visible + await expect( + page.getByRole('button', { name: 'Remove attachment' }), + ).toBeHidden(); + + // Textarea still there + await expect(chatInput).toBeVisible(); + }); +});