Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 0 additions & 26 deletions .eslintrc.json

This file was deleted.

10 changes: 5 additions & 5 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"sonarlint.connectedMode.project": {
"connectionId": "hopper",
"projectKey": "fhswf_openai-ui_AY5mLbV1WNlYFiIpzZdP"
},
"i18n-ally.localesPaths": [
"src/i18n"
],
"react-i18n.i18nPaths": "src\\i18n"
"react-i18n.i18nPaths": "src\\i18n",
"sonarlint.connectedMode.project": {
"connectionId": "sonarqube-fh-swf-cloud",
"projectKey": "fhswf_openai-ui_3d78bdaa-d0c9-4d19-a2c6-65fa429b1112"
}
}
1,017 changes: 19 additions & 998 deletions CHANGELOG.md

Large diffs are not rendered by default.

60 changes: 60 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import js from "@eslint/js";
import globals from "globals";
import reactPlugin from "eslint-plugin-react";
import prettierPlugin from "eslint-plugin-prettier";
import prettierConfig from "eslint-config-prettier";
import { FlatCompat } from "@eslint/eslintrc";
import path from "node:path";
import { fileURLToPath } from "node:url";
import tseslint from "typescript-eslint";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const compat = new FlatCompat({
baseDirectory: __dirname,
});

export default [
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ["**/*.{js,jsx,mjs,cjs,ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
sourceType: "module",
globals: {
...globals.browser,
...globals.es2020,
Atomics: "readonly",
SharedArrayBuffer: "readonly",
},
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
plugins: {
react: reactPlugin,
prettier: prettierPlugin,
},
rules: {
...prettierConfig.rules,
"prettier/prettier": "error",
"react/react-in-jsx-scope": "off", // Not needed in React 17+
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/no-empty-object-type": "off",
"@typescript-eslint/no-unused-expressions": "off",
"no-unused-vars": "off", // Use typescript-eslint version
},
settings: {
react: {
version: "detect",
},
},
},
// Storybook specific configuration
...compat.extends("plugin:storybook/recommended"),
];
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
}
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@playwright/test": "^1.56.1",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^13.0.1",
Expand All @@ -176,8 +177,12 @@
"css-loader": "^6.7.3",
"cypress": "^14.2.1",
"eslint": "^9.20.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-storybook": "^10.1.0",
"file-loader": "^6.2.0",
"globals": "^16.5.0",
"html-webpack-plugin": "^5.5.0",
"less": "^4.2.2",
"less-loader": "^11.1.0",
Expand All @@ -190,8 +195,10 @@
"prop-types": "^15.8.1",
"semantic-release": "^25.0.2",
"semantic-release-replace-plugin": "^1.2.7",
"storybook": "^10.1.0",
"style-loader": "^4.0.0",
"typescript": "^5.8.2",
"typescript-eslint": "^8.48.0",
"url-loader": "^4.1.1",
"vite": "^6.3.4",
"vite-plugin-compression2": "^1.3.3",
Expand Down
45 changes: 29 additions & 16 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export default defineConfig({
/* Base URL to use in actions like `await page.goto('')`. */
baseURL: 'http://localhost:5173',

locale: 'de-DE',

/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
Expand Down Expand Up @@ -61,27 +63,38 @@ export default defineConfig({
storageState: 'playwright/.auth/user.json'
},
dependencies: ['setup'],

},

/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
{
name: 'Mobile Chrome',
use: {
...devices['Pixel 5'],
storageState: 'playwright/.auth/user.json'
},
dependencies: ['setup'],
},
{
name: 'Mobile Safari',
use: {
...devices['iPhone 12'],
storageState: 'playwright/.auth/user.json'
},
dependencies: ['setup'],
},

/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
{
name: 'Microsoft Edge',
use: { ...devices['Desktop Edge'], channel: 'msedge', storageState: 'playwright/.auth/user.json' },
dependencies: ['setup'],
},
{
name: 'Google Chrome',
use: { ...devices['Desktop Chrome'], channel: 'chrome', storageState: 'playwright/.auth/user.json' },
dependencies: ['setup'],
},
],

/* Run your local dev server before starting the tests */
Expand Down
33 changes: 33 additions & 0 deletions playwright/tests/authorized/image_delete.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { test, expect } from '../baseFixtures';

test('Image Delete', async ({ page, browserName }) => {
test.skip(browserName === 'webkit', "Skipping Webkit due to issues with OPFS");

await page.goto("");

// Ensure chat is ready
await expect(page.getByTestId('ChatTextArea')).toBeVisible();

// Create a dummy image file
const buffer = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64');

// Upload an image
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByTestId('UploadFileBtn').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles({
name: 'test_image_delete.png',
mimeType: 'image/png',
buffer: buffer,
});

// Verify image is visible
const imageLocator = page.locator('img[alt="test_image_delete.png"]');
await expect(imageLocator).toBeVisible();

// Click delete button
await page.getByLabel('Delete image').click();

// Verify image is gone
await expect(imageLocator).not.toBeVisible();
});
54 changes: 54 additions & 0 deletions playwright/tests/authorized/image_drop.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { test, expect } from '../baseFixtures';


test('Image Drop', async ({ page, browserName }) => {
test.skip(browserName === 'webkit', "Skipping Webkit due to issues with OPFS");

await page.goto("");

// Ensure chat is ready
await expect(page.getByTestId('ChatTextArea')).toBeVisible();

// Create a dummy image file
const buffer = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64');

// Test clicking the upload button
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByTestId('UploadFileBtn').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles({
name: 'test_image_click.png',
mimeType: 'image/png',
buffer: buffer,
});
await expect(page.locator('img[alt="test_image_click.png"]')).toBeVisible();

// Test drag and drop
// We can use dispatchEvent to simulate a drop event on the dropzone
const dataTransfer = await page.evaluateHandle((data) => {
const dt = new DataTransfer();
const binaryString = atob(data.buffer);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.codePointAt(i) || 0;
}
const file = new File([bytes], 'test_image_drop.png', { type: 'image/png' });
dt.items.add(file);
return dt;
}, { buffer: buffer.toString('base64') });

await page.getByTestId('file-input').dispatchEvent('drop', { dataTransfer });
await expect(page.locator('img[alt="test_image_drop.png"]')).toBeVisible();

// Test sending the message with the image
// This verifies that the OPFS image is correctly converted to base64 and sent to the API
// without causing a 400 error.
await page.getByTestId('ChatTextArea').fill("Test message with image");
await page.getByTestId('SendMessageBtn').click();

// Wait for the user message to appear in the chat history
// It should contain the image
const userMessage = page.locator('[data-testid^="ChatMessage-"]').filter({ has: page.locator('img[alt="test_image_drop.png"]') });
await expect(userMessage).toBeVisible();
await expect(userMessage.locator('img[alt="test_image_drop.png"]')).toBeVisible({ timeout: 15000 });
});
67 changes: 67 additions & 0 deletions playwright/tests/authorized/image_generation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { test, expect } from '../baseFixtures';

test('Image Generation', async ({ page, browserName }) => {
test.skip(browserName === 'webkit', "Skipping Webkit due to issues with OPFS");

// Mock the OpenAI API response
await page.route('**/v1/responses', async route => {
const responseBody = [
{
type: 'response.created',
response: {
id: 'resp_mock_123',
}
},
{
type: 'response.output_item.done',
item: {
id: 'item_mock_123',
output_format: 'png',
result: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', // 1x1 red pixel
}
},
{
type: 'response.completed',
response: {
usage: {
total_tokens: 10,
input_tokens: 5,
output_tokens: 5
}
}
}
];

// Simulate SSE stream
const stream = responseBody.map(event => `data: ${JSON.stringify(event)}\n\n`).join('');

await route.fulfill({
status: 200,
contentType: 'text/event-stream',
body: stream,
});
});

// Enable console logging
page.on('console', msg => console.log(`BROWSER LOG: ${msg.text()}`));

await page.goto("");

// Ensure chat is ready
await expect(page.getByTestId('ChatTextArea')).toBeVisible();

// Send a message to trigger image generation
await page.getByTestId('ChatTextArea').fill('Generate a red pixel');
await page.getByTestId('SendMessageBtn').click();

// Check for error toast
const errorToast = page.locator('.chakra-toast');
if (await errorToast.isVisible()) {
console.log('Error Toast found:', await errorToast.textContent());
}

// Verify the image is displayed
// Verify the image is displayed
// The key in message.images is the item id
await expect(page.getByTestId('generated-image-item_mock_123')).toBeVisible({ timeout: 10000 });
});
Loading
Loading