Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 84ece21066 | |||
| f5a1b72913 | |||
| 9e37f71818 |
@@ -12,6 +12,15 @@ 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
|
||||
|
||||
- ⬆️(dependencies) upgrade Next.js 15 to 16, upgrade python dependencies
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(back) add missing color option for project colors
|
||||
|
||||
## [0.0.14] - 2026-03-11
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ class Migration(migrations.Migration):
|
||||
("color_7", "Color 7"),
|
||||
("color_8", "Color 8"),
|
||||
("color_9", "Color 9"),
|
||||
("color_10", "Color 10"),
|
||||
],
|
||||
help_text="Project icon color",
|
||||
max_length=20,
|
||||
|
||||
@@ -52,6 +52,7 @@ class ChatProjectColor(models.TextChoices):
|
||||
COLOR_7 = "color_7", "Color 7"
|
||||
COLOR_8 = "color_8", "Color 8"
|
||||
COLOR_9 = "color_9", "Color 9"
|
||||
COLOR_10 = "color_10", "Color 10"
|
||||
|
||||
|
||||
class ChatProject(BaseModel):
|
||||
|
||||
@@ -78,6 +78,24 @@ def test_create_project_with_llm_instructions(api_client):
|
||||
assert project.llm_instructions == "Always answer in French."
|
||||
|
||||
|
||||
@pytest.mark.parametrize("color", ChatProjectColor)
|
||||
def test_create_project_with_each_color(api_client, color):
|
||||
"""Test creating a project with each available color."""
|
||||
user = UserFactory()
|
||||
url = "/api/v1.0/projects/"
|
||||
data = {
|
||||
"title": "Color Project",
|
||||
"icon": ChatProjectIcon.FOLDER,
|
||||
"color": color,
|
||||
}
|
||||
api_client.force_login(user)
|
||||
response = api_client.post(url, data, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
project = ChatProject.objects.get(id=response.data["id"])
|
||||
assert project.color == color
|
||||
|
||||
|
||||
def test_create_project_anonymous(api_client):
|
||||
"""Test creating a project as an anonymous user returns a 401 error."""
|
||||
url = "/api/v1.0/projects/"
|
||||
|
||||
@@ -368,6 +368,7 @@ class Base(BraveSettings, Configuration):
|
||||
"django.contrib.staticfiles",
|
||||
# OIDC third party
|
||||
"mozilla_django_oidc",
|
||||
"lasuite.malware_detection",
|
||||
]
|
||||
|
||||
# Cache
|
||||
|
||||
+31
-30
@@ -23,15 +23,15 @@ description = "An application to chat with your own AI."
|
||||
keywords = ["Django", "AI", "Chatbot", "OpenAI", "Pydantic AI", "Conversations"]
|
||||
license = { file = "LICENSE" }
|
||||
readme = "README.md"
|
||||
requires-python = "~=3.14.3"
|
||||
requires-python = "~=3.13.0"
|
||||
dependencies = [
|
||||
"deprecated",
|
||||
"beautifulsoup4==4.14.3",
|
||||
"boto3==1.42.68",
|
||||
"beautifulsoup4==4.14.2",
|
||||
"boto3==1.40.73",
|
||||
"Brotli==1.2.0",
|
||||
"django-configurations==2.5.1",
|
||||
"django-cors-headers==4.9.0",
|
||||
"django-countries==8.2.0",
|
||||
"django-countries==8.1.0",
|
||||
"django-filter==25.2",
|
||||
"django-lasuite[all]==0.0.25",
|
||||
"django-parler==2.3",
|
||||
@@ -39,35 +39,35 @@ dependencies = [
|
||||
"django-redis==6.0.0",
|
||||
"django-storages[s3]==1.14.6",
|
||||
"django-timezone-field>=5.1",
|
||||
"django==6.0.3",
|
||||
"django==5.2.12",
|
||||
"djangorestframework==3.16.1",
|
||||
"drf_spectacular==0.29.0",
|
||||
"dockerflow==2026.3.4",
|
||||
"dockerflow==2024.4.2",
|
||||
"easy_thumbnails==2.10.1",
|
||||
"factory_boy==3.3.3",
|
||||
"gunicorn==25.1.0",
|
||||
"gunicorn==23.0.0",
|
||||
"jaraco.context>=6.1.0",
|
||||
"jsonschema==4.26.0",
|
||||
"langfuse==4.0.0",
|
||||
"jsonschema==4.25.1",
|
||||
"langfuse==3.10.0",
|
||||
"lxml==5.4.0",
|
||||
"markdown==3.10.2",
|
||||
"markdown==3.10",
|
||||
"markitdown==0.0.2",
|
||||
"mozilla-django-oidc==5.0.2",
|
||||
"mozilla-django-oidc==4.0.1",
|
||||
"nested-multipart-parser==1.6.0",
|
||||
"posthog==7.9.12",
|
||||
"pydantic==2.12.5",
|
||||
"pydantic-ai-slim[openai,mistral,mcp,evals,logfire]==1.68.0",
|
||||
"psycopg[binary]==3.3.3",
|
||||
"PyJWT==2.10.1",
|
||||
"posthog==7.0.0",
|
||||
"pydantic==2.12.4",
|
||||
"pydantic-ai-slim[openai,mistral,mcp,evals,logfire]==1.62.0",
|
||||
"psycopg[binary]==3.2.12",
|
||||
"PyJWT==2.12.0",
|
||||
"python-magic==0.4.27",
|
||||
"redis<6.0.0",
|
||||
"requests==2.32.5",
|
||||
"semchunk==3.2.5",
|
||||
"sentry-sdk==2.54.0",
|
||||
"sentry-sdk==2.44.0",
|
||||
"trafilatura==2.0.0",
|
||||
"uvicorn==0.41.0",
|
||||
"whitenoise==6.12.0",
|
||||
"pypdf==6.8.0",
|
||||
"uvicorn==0.38.0",
|
||||
"whitenoise==6.11.0",
|
||||
"pypdf==6.9.1",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -78,27 +78,27 @@ dependencies = [
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"dirty-equals==0.11",
|
||||
"dirty-equals==0.10.0",
|
||||
"django-extensions==4.1",
|
||||
"django-test-migrations==1.5.0",
|
||||
"drf-spectacular-sidecar==2026.3.1",
|
||||
"drf-spectacular-sidecar==2025.10.1",
|
||||
"freezegun==1.5.5",
|
||||
"ipdb==0.13.13",
|
||||
"ipython==9.11.0",
|
||||
"pyfakefs==6.1.5",
|
||||
"pylint-django==2.7.0",
|
||||
"ipython==9.7.0",
|
||||
"pyfakefs==5.10.2",
|
||||
"pylint-django==2.6.1",
|
||||
"pylint==3.3.9",
|
||||
"pylint-pydantic==0.4.1",
|
||||
"pytest-asyncio==1.3.0",
|
||||
"pytest-cov==7.0.0",
|
||||
"pytest-django==4.12.0",
|
||||
"pytest==9.0.2",
|
||||
"pytest-django==4.11.1",
|
||||
"pytest==9.0.1",
|
||||
"pytest-icdiff==0.9",
|
||||
"pytest-xdist==3.8.0",
|
||||
"responses==0.26.0",
|
||||
"responses==0.25.8",
|
||||
"respx==0.22.0",
|
||||
"ruff==0.15.6",
|
||||
"types-requests==2.32.4.20260107",
|
||||
"ruff==0.14.5",
|
||||
"types-requests==2.32.4.20250913",
|
||||
]
|
||||
|
||||
[tool.setuptools]
|
||||
@@ -115,6 +115,7 @@ required-environments = [
|
||||
]
|
||||
override-dependencies = [
|
||||
"cryptography>=46.0.5", # CVE-2026-26007
|
||||
"joserfc>=1.6.3", # CVE-2026-27932
|
||||
"pillow>=12.1.1", #CVE-2026-25990
|
||||
]
|
||||
|
||||
|
||||
Generated
+470
-503
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
const crypto = require('crypto');
|
||||
|
||||
const { InjectManifest } = require('workbox-webpack-plugin');
|
||||
|
||||
const buildId = crypto.randomBytes(256).toString('hex').slice(0, 8);
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
@@ -12,44 +10,22 @@ const nextConfig = {
|
||||
unoptimized: true,
|
||||
},
|
||||
compiler: {
|
||||
// Enables the styled-components SWC transform
|
||||
styledComponents: true,
|
||||
},
|
||||
generateBuildId: () => buildId,
|
||||
env: {
|
||||
NEXT_PUBLIC_BUILD_ID: buildId,
|
||||
},
|
||||
webpack(config, { isServer }) {
|
||||
// Grab the existing rule that handles SVG imports
|
||||
const fileLoaderRule = config.module.rules.find((rule) =>
|
||||
rule.test?.test?.('.svg'),
|
||||
);
|
||||
|
||||
config.module.rules.push(
|
||||
// Reapply the existing rule, but only for svg imports ending in ?url
|
||||
{
|
||||
...fileLoaderRule,
|
||||
test: /\.svg$/i,
|
||||
resourceQuery: /url/, // *.svg?url
|
||||
turbopack: {
|
||||
rules: {
|
||||
'*.svg': {
|
||||
loaders: ['@svgr/webpack'],
|
||||
as: '*.js',
|
||||
},
|
||||
// Convert all other *.svg imports to React components
|
||||
{
|
||||
test: /\.svg$/i,
|
||||
issuer: fileLoaderRule.issuer,
|
||||
resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] }, // exclude if *.svg?url
|
||||
use: ['@svgr/webpack'],
|
||||
},
|
||||
);
|
||||
|
||||
// Modify the file loader rule to ignore *.svg, since we have it handled now.
|
||||
fileLoaderRule.exclude = /\.svg$/i;
|
||||
|
||||
// Formula rendering in markdown, replace dollar-sign with \(...\) and \[...\]
|
||||
// https://github.com/remarkjs/remark-math/issues/39#issuecomment-2636184992
|
||||
config.resolve.alias['micromark-extension-math'] =
|
||||
'micromark-extension-llm-math';
|
||||
|
||||
return config;
|
||||
},
|
||||
resolveAlias: {
|
||||
'micromark-extension-math': 'micromark-extension-llm-math',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -37,11 +37,11 @@
|
||||
"lottie-react": "^2.4.1",
|
||||
"luxon": "3.6.1",
|
||||
"micromark-extension-llm-math": "3.1.1-20250610",
|
||||
"next": "15.3.9",
|
||||
"next": "16.2.0",
|
||||
"posthog-js": "1.249.3",
|
||||
"react": "19.2.1",
|
||||
"react": "19.2.4",
|
||||
"react-aria-components": "1.9.0",
|
||||
"react-dom": "19.2.1",
|
||||
"react-dom": "19.2.4",
|
||||
"react-i18next": "15.5.2",
|
||||
"react-intersection-observer": "9.16.0",
|
||||
"react-markdown": "10.1.0",
|
||||
@@ -65,22 +65,23 @@
|
||||
"@types/lodash": "4.17.17",
|
||||
"@types/luxon": "3.6.2",
|
||||
"@types/node": "*",
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"cross-env": "7.0.3",
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"dotenv": "16.5.0",
|
||||
"eslint": "^9",
|
||||
"eslint-config-conversations": "*",
|
||||
"eslint-config-next": "16.2.0",
|
||||
"fetch-mock": "9.11.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"node-fetch": "2.7.0",
|
||||
"prettier": "3.5.3",
|
||||
"sass": "^1.97.3",
|
||||
"stylelint": "16.20.0",
|
||||
"stylelint-config-standard": "38.0.0",
|
||||
"stylelint-prettier": "5.0.3",
|
||||
"typescript": "*",
|
||||
"webpack": "5.99.9",
|
||||
"workbox-webpack-plugin": "7.1.0"
|
||||
"typescript": "*"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -12,14 +12,10 @@
|
||||
"test:ui::chromium": "yarn test:ui --project=chromium"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.52.0",
|
||||
"@playwright/test": "1.56.0",
|
||||
"@types/node": "*",
|
||||
"@types/pdf-parse": "1.1.5",
|
||||
"eslint-config-conversations": "*",
|
||||
"typescript": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"convert-stream": "1.0.2",
|
||||
"pdf-parse": "1.1.1"
|
||||
}
|
||||
"dependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
declare module 'convert-stream' {
|
||||
export function toBuffer(
|
||||
readableStream: NodeJS.ReadableStream,
|
||||
): Promise<Buffer>;
|
||||
}
|
||||
@@ -27,14 +27,17 @@
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/node": "22.15.29",
|
||||
"@types/react": "19.1.6",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "8.33.1",
|
||||
"@typescript-eslint/parser": "8.33.1",
|
||||
"eslint": "^9.0.0",
|
||||
"react": "19.2.1",
|
||||
"react-dom": "19.2.1",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"typescript": "5.8.3",
|
||||
"minimatch": ">=10.2.1"
|
||||
"minimatch": ">=10.2.1",
|
||||
"flatted": ">=3.4.0",
|
||||
"rollup": ">=4.59.0",
|
||||
"undici": ">=7.24.0"
|
||||
}
|
||||
}
|
||||
|
||||
+548
-1182
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user