-
Notifications
You must be signed in to change notification settings - Fork 19
Add more dedicated e2e tests for file paste in chat input #329
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
providenz
wants to merge
1
commit into
main
Choose a base branch
from
providenz/paste-e2e
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
270 changes: 270 additions & 0 deletions
270
src/frontend/apps/e2e/__tests__/app-conversations/file-paste.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| }); | ||
|
providenz marked this conversation as resolved.
|
||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.