From 6db39042786bea7614e1366a940b5bc4fcb3276a Mon Sep 17 00:00:00 2001 From: tobiasbkraemer Date: Fri, 12 Dec 2025 14:08:50 +0100 Subject: [PATCH 1/7] =?UTF-8?q?Stress=20performance=20tests=20virker.=20Hu?= =?UTF-8?q?sk=20at=20k=C3=B8re=20dem=20individuelt=20for=20ikke=20at=20ove?= =?UTF-8?q?rbelaste=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 17 ++++++++++++++++- backend/app/db/mysql.py | 4 ++-- docker-compose.yml | 4 +++- performance-tests/README.md | 17 +++++++++++++++++ performance-tests/config.js | 14 ++++++++++++++ performance-tests/load.js | 24 +++++++++++++++++++++++ performance-tests/soak.js | 24 +++++++++++++++++++++++ performance-tests/spike.js | 29 ++++++++++++++++++++++++++++ performance-tests/stress.js | 38 +++++++++++++++++++++++++++++++++++++ 9 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 performance-tests/README.md create mode 100644 performance-tests/config.js create mode 100644 performance-tests/load.js create mode 100644 performance-tests/soak.js create mode 100644 performance-tests/spike.js create mode 100644 performance-tests/stress.js diff --git a/README.md b/README.md index 7c3262d..0baf2cc 100644 --- a/README.md +++ b/README.md @@ -121,4 +121,19 @@ poetry run safety scan poetry run bandit -r app poetry run ruff check . poetry run mypy app -``` \ No newline at end of file +``` + +## Performance tests (k6) +- Kræver backend kørende (default `http://localhost:8000`, kan overstyres med `BASE_URL`). +- Lokal k6: + - `k6 run --env BASE_URL=http://localhost:8000 performance-tests/load.js` + - `k6 run --env BASE_URL=http://localhost:8000 performance-tests/spike.js` + - `k6 run --env BASE_URL=http://localhost:8000 performance-tests/stress.js` + - `k6 run --env BASE_URL=http://localhost:8000 performance-tests/soak.js` +- Via Docker (uden k6 installeret): + `docker run --rm -it --network host -v ${PWD}/performance-tests:/scripts grafana/k6 run --env BASE_URL=http://localhost:8000 /scripts/.js` +- Nuværende mål (justér efter behov): + - Load: p95 < 2s @ 50 VU + - Spike: p95 < 6s @ 150 VU + - Stress: p95 < 4.5s @ 150 VU + - Soak: 20 VU i 1 time (overvåg for fejlrate/drift) diff --git a/backend/app/db/mysql.py b/backend/app/db/mysql.py index 91becbd..90c5681 100644 --- a/backend/app/db/mysql.py +++ b/backend/app/db/mysql.py @@ -17,8 +17,8 @@ echo=False, # SQL query logging pool_pre_ping=True, # Verify connections before using pool_recycle=300, # Recycle connections after 5 minutes - pool_size=5, # Connection pool size - max_overflow=10, # Max connections beyond pool_size + pool_size=15, # Connection pool size (moderate to avoid max_connections) + max_overflow=30, # Max connections beyond pool_size connect_args={ 'connect_timeout': 10, # Connection timeout in seconds } diff --git a/docker-compose.yml b/docker-compose.yml index 64e9678..3ae6104 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: backend: build: ./backend - command: poetry run uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload --no-access-log + command: poetry run uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4 --no-access-log volumes: - ./backend:/code ports: @@ -51,6 +51,8 @@ services: retries: 2 interval: 15s start_period: 30s + command: --max_connections=500 + volumes: mysql_data: diff --git a/performance-tests/README.md b/performance-tests/README.md new file mode 100644 index 0000000..55c9560 --- /dev/null +++ b/performance-tests/README.md @@ -0,0 +1,17 @@ +# Performance tests + +All scripts use the same base URL helper. Default: `http://localhost:8000` (override with `BASE_URL`). + +Run locally (k6 installed): +- `k6 run --env BASE_URL=http://localhost:8000 performance-tests/load.js` +- `k6 run --env BASE_URL=http://localhost:8000 performance-tests/spike.js` +- `k6 run --env BASE_URL=http://localhost:8000 performance-tests/stress.js` +- `k6 run --env BASE_URL=http://localhost:8000 performance-tests/soak.js` + +Via Docker (no local k6), PowerShell: +- `docker run --rm -it --network host -v ${PWD}/performance-tests:/scripts grafana/k6 run --env BASE_URL=http://localhost:8000 /scripts/.js` + +Notes: +- Stress is capped at 150 VU with p95<4s target (matches what backend currently klarer). +- Spike går til 150 VU med et mere lempeligt p95<6s target. +- Soak kører 1 time; stop den når du har nok data. diff --git a/performance-tests/config.js b/performance-tests/config.js new file mode 100644 index 0000000..c416bb0 --- /dev/null +++ b/performance-tests/config.js @@ -0,0 +1,14 @@ +import { check } from 'k6'; + +// Base URL for the API. Override with: k6 run --env BASE_URL=http://backend:8000 performance-tests/stress.js +const rawBaseUrl = __ENV.BASE_URL || 'http://localhost:8000'; + +// Remove trailing slash to avoid double slashes when building endpoints +export const BASE_URL = rawBaseUrl.replace(/\/$/, ''); +// Include trailing slash before query params to avoid 307 redirects from FastAPI +export const PRODUCTS_URL = `${BASE_URL}/api/products/?page=1&size=5`; +export const HEALTH_URL = `${BASE_URL}/health`; + +export function ensureOk(response, label = 'status 200') { + return check(response, { [label]: (r) => r.status === 200 }); +} diff --git a/performance-tests/load.js b/performance-tests/load.js new file mode 100644 index 0000000..5001ecd --- /dev/null +++ b/performance-tests/load.js @@ -0,0 +1,24 @@ +import http from 'k6/http'; +import { sleep } from 'k6'; +import { PRODUCTS_URL, HEALTH_URL, ensureOk } from './config.js'; + +// Moderate, steady-state load test +export const options = { + vus: 50, + duration: '1m', + thresholds: { + http_req_failed: ['rate<0.01'], + http_req_duration: ['p(95)<2000'], + }, +}; + +export function setup() { + const health = http.get(HEALTH_URL); + ensureOk(health, 'health 200'); +} + +export default function () { + const res = http.get(PRODUCTS_URL); + ensureOk(res, 'products 200'); + sleep(1); +} diff --git a/performance-tests/soak.js b/performance-tests/soak.js new file mode 100644 index 0000000..e64adc3 --- /dev/null +++ b/performance-tests/soak.js @@ -0,0 +1,24 @@ +import http from 'k6/http'; +import { sleep } from 'k6'; +import { PRODUCTS_URL, HEALTH_URL, ensureOk } from './config.js'; + +// Long-running soak test to reveal memory leaks or slow degradation +export const options = { + vus: 20, + duration: '1h', + thresholds: { + http_req_failed: ['rate<0.01'], + http_req_duration: ['p(95)<2000'], + }, +}; + +export function setup() { + const health = http.get(HEALTH_URL); + ensureOk(health, 'health 200'); +} + +export default function () { + const res = http.get(PRODUCTS_URL); + ensureOk(res, 'products 200'); + sleep(1); +} diff --git a/performance-tests/spike.js b/performance-tests/spike.js new file mode 100644 index 0000000..74a3de8 --- /dev/null +++ b/performance-tests/spike.js @@ -0,0 +1,29 @@ +import http from 'k6/http'; +import { sleep } from 'k6'; +import { PRODUCTS_URL, HEALTH_URL, ensureOk } from './config.js'; + +// Sudden spike test to observe auto-scaling or throttling behavior +export const options = { + stages: [ + { duration: '5s', target: 1 }, + { duration: '12s', target: 90 }, + { duration: '30s', target: 90 }, + { duration: '10s', target: 0 }, + ], + thresholds: { + http_req_failed: ['rate<0.01'], + // Allow a bit more slack for spike; tighten later if backend improves + http_req_duration: ['p(95)<6000'], + }, +}; + +export function setup() { + const health = http.get(HEALTH_URL); + ensureOk(health, 'health 200'); +} + +export default function () { + const res = http.get(PRODUCTS_URL); + ensureOk(res, 'products 200'); + sleep(0.5); +} diff --git a/performance-tests/stress.js b/performance-tests/stress.js new file mode 100644 index 0000000..2cd2385 --- /dev/null +++ b/performance-tests/stress.js @@ -0,0 +1,38 @@ +import http from 'k6/http'; +import { sleep } from 'k6'; +import { PRODUCTS_URL, HEALTH_URL, ensureOk } from './config.js'; + +export const options = { + stages: [ + { duration: '30s', target: 20 }, + { duration: '30s', target: 50 }, + { duration: '30s', target: 100 }, + { duration: '30s', target: 150 }, + { duration: '30s', target: 150 }, + { duration: '30s', target: 0 }, + ], + thresholds: { + http_req_failed: ['rate<0.01'], + http_req_duration: ['p(95)<4500'], // slight slack; adjust if SLA tightens + }, +}; + +export function setup() { + // Wait for backend to be ready (handles cold starts after compose up) + const maxAttempts = 60; + for (let i = 0; i < maxAttempts; i += 1) { + const health = http.get(HEALTH_URL); + if (health.status === 200) { + ensureOk(health, 'health 200'); + return; + } + sleep(1); + } + throw new Error('Backend health check did not become ready within 60s'); +} + +export default function () { + const res = http.get(PRODUCTS_URL); + ensureOk(res, 'products 200'); + sleep(1); +} From 5f4b555eb5bcb0c5159b3098bacb37a21a9d57e1 Mon Sep 17 00:00:00 2001 From: VictorHanert Date: Sun, 14 Dec 2025 12:16:55 +0100 Subject: [PATCH 2/7] Updated readme with install guide --- performance-tests/README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/performance-tests/README.md b/performance-tests/README.md index 55c9560..3f2ef7d 100644 --- a/performance-tests/README.md +++ b/performance-tests/README.md @@ -2,7 +2,16 @@ All scripts use the same base URL helper. Default: `http://localhost:8000` (override with `BASE_URL`). -Run locally (k6 installed): +Install k6: +- Via Homebrew (macOS): `brew install k6` +- Via Chocolatey (Windows): `choco install k6` + +Verify installation: +```sh +k6 version +``` + +Run locally: - `k6 run --env BASE_URL=http://localhost:8000 performance-tests/load.js` - `k6 run --env BASE_URL=http://localhost:8000 performance-tests/spike.js` - `k6 run --env BASE_URL=http://localhost:8000 performance-tests/stress.js` From 597bce6bc76f04e8e831262d0afe88175192093c Mon Sep 17 00:00:00 2001 From: VictorHanert Date: Sun, 14 Dec 2025 12:21:10 +0100 Subject: [PATCH 3/7] Fixed typo in e2e test --- frontend/e2e/messaging.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/e2e/messaging.spec.js b/frontend/e2e/messaging.spec.js index 62f1502..4e3a7b0 100644 --- a/frontend/e2e/messaging.spec.js +++ b/frontend/e2e/messaging.spec.js @@ -2,7 +2,7 @@ import { test, expect } from './fixtures/auth.js'; test('User can open a product page', async ({ loggedInPage, request }) => { // Fetch a real product id to avoid clicking random nav links - const res = await request.get('http://localhost:8000/api/products?page=1&size=1'); + const res = await request.get('http://localhost:8000/api/products/?page=1&size=1'); const data = await res.json(); const firstProductId = data?.products?.[0]?.id; if (!firstProductId) throw new Error('No products returned from API'); From 6ceac729e48c4a8890a7906695867d86d7a3b4d8 Mon Sep 17 00:00:00 2001 From: UlrikLn Date: Sun, 14 Dec 2025 13:41:33 +0100 Subject: [PATCH 4/7] Better product Testing --- frontend/e2e/products.spec.js | 162 +++++++++++++++++++++++++++++++++- 1 file changed, 159 insertions(+), 3 deletions(-) diff --git a/frontend/e2e/products.spec.js b/frontend/e2e/products.spec.js index c156117..e42ac1b 100644 --- a/frontend/e2e/products.spec.js +++ b/frontend/e2e/products.spec.js @@ -1,6 +1,162 @@ import { test, expect } from './fixtures/auth.js'; -test('User can navigate to create product page', async ({ loggedInPage }) => { - await loggedInPage.click('text=Create Listing'); - await expect(loggedInPage).toHaveURL(/create-product/); +test.describe('Product Viewing', () => { + test('User can open a product page', async ({ loggedInPage, request }) => { + // Fetch a real product id, or create one if none exist + let res = await request.get('http://localhost:8000/api/products/?page=1&size=1'); + let data = await res.json(); + let firstProductId = data?.products?.[0]?.id; + + // If no products exist, create one + if (!firstProductId) { + // First get categories to use in product creation + const categoriesRes = await request.get('http://localhost:8000/api/products/categories'); + const categories = await categoriesRes.json(); + const categoryId = categories[0]?.id; + + if (!categoryId) throw new Error('No categories available to create product'); + + // Create a test product + const createRes = await request.post('http://localhost:8000/api/products/', { + data: { + title: `Test Bicycle ${Date.now()}`, + description: 'Test bicycle for E2E test', + price_amount: 1000, + price_currency: 'DKK', + category_id: categoryId, + }, + }); + + const newProduct = await createRes.json(); + firstProductId = newProduct.id; + } + + await loggedInPage.goto(`/products/${firstProductId}`); + + // Assert product detail page loaded + await expect(loggedInPage).toHaveURL(new RegExp(`/products/${firstProductId}`)); + await expect(loggedInPage.getByRole('heading', { level: 1 })).toBeVisible(); + }); +}); + +test.describe('Product Creation', () => { + test('User can navigate to create product page', async ({ loggedInPage }) => { + await loggedInPage.click('text=Create Listing'); + await expect(loggedInPage).toHaveURL(/create-product/); + await expect(loggedInPage.getByRole('heading', { name: 'Sell Your Bicycle' })).toBeVisible(); + }); + + test('User can create a product with required fields only', async ({ loggedInPage }) => { + // Navigate to create product page + await loggedInPage.goto('/create-product'); + await loggedInPage.waitForLoadState('networkidle'); + + // Fill in required fields + const uniqueTitle = `Test Bicycle ${Date.now()}`; + await loggedInPage.fill('input[name="title"]', uniqueTitle); + await loggedInPage.fill('textarea[name="description"]', 'This is a test bicycle in excellent condition. Perfect for daily commuting.'); + await loggedInPage.fill('input[name="price_amount"]', '1500'); + + // Select a category (assuming at least one exists) + await loggedInPage.selectOption('select[name="category_id"]', { index: 1 }); + + // Submit the form + await loggedInPage.click('button[type="submit"]'); + + // Wait for success and redirect to product detail page + await loggedInPage.waitForURL(/\/products\/\d+/, { timeout: 10000 }); + + // Verify product details are displayed + await expect(loggedInPage.getByText(uniqueTitle)).toBeVisible(); + await expect(loggedInPage.getByText('This is a test bicycle in excellent condition')).toBeVisible(); + }); + + test('User can create a product with optional fields', async ({ loggedInPage }) => { + // Navigate to create product page + await loggedInPage.goto('/create-product'); + await loggedInPage.waitForLoadState('networkidle'); + + // Fill in required fields + const uniqueTitle = `Premium Bicycle ${Date.now()}`; + await loggedInPage.fill('input[name="title"]', uniqueTitle); + await loggedInPage.fill('textarea[name="description"]', 'High-quality bicycle with advanced features and excellent build quality.'); + await loggedInPage.fill('input[name="price_amount"]', '2500'); + await loggedInPage.selectOption('select[name="category_id"]', { index: 1 }); + + // Fill in optional fields + await loggedInPage.selectOption('select[name="condition"]', 'like_new'); + await loggedInPage.fill('input[name="quantity"]', '2'); + + // Submit the form + await loggedInPage.click('button[type="submit"]'); + + // Wait for success and redirect + await loggedInPage.waitForURL(/\/products\/\d+/, { timeout: 10000 }); + + // Verify product was created + await expect(loggedInPage.getByText(uniqueTitle)).toBeVisible(); + }); + + test('Form validation prevents submission with missing required fields', async ({ loggedInPage }) => { + await loggedInPage.goto('/create-product'); + await loggedInPage.waitForLoadState('networkidle'); + + // Try to submit without filling anything + await loggedInPage.click('button[type="submit"]'); + + // Should show validation errors and stay on the same page + await expect(loggedInPage).toHaveURL(/create-product/); + + // Check for validation error messages + await expect(loggedInPage.getByText(/Title is required|required/i).first()).toBeVisible({ timeout: 5000 }); + }); + + test('Form validation shows error for invalid price', async ({ loggedInPage }) => { + await loggedInPage.goto('/create-product'); + await loggedInPage.waitForLoadState('networkidle'); + + // Fill required fields with invalid price + await loggedInPage.fill('input[name="title"]', 'Test Bicycle'); + await loggedInPage.fill('textarea[name="description"]', 'Test description for validation'); + await loggedInPage.fill('input[name="price_amount"]', '-10'); + await loggedInPage.selectOption('select[name="category_id"]', { index: 1 }); + + // Try to submit + await loggedInPage.click('button[type="submit"]'); + + // Should show validation error for price + await expect(loggedInPage).toHaveURL(/create-product/); + }); + + test('User can add and remove images when creating a product', async ({ loggedInPage }) => { + await loggedInPage.goto('/create-product'); + await loggedInPage.waitForLoadState('networkidle'); + + // Look for file input (it might be hidden, so we'll use setInputFiles directly) + const fileInput = loggedInPage.locator('input[type="file"]'); + + if (await fileInput.count() > 0) { + // Create a simple test image buffer + const testImagePath = '/tmp/test-bike-image.png'; + + // Note: In a real scenario, you'd have actual test images + // For now, we're just checking if the file input exists + await expect(fileInput).toBeAttached(); + } + }); + + test('Character count updates as user types in title and description', async ({ loggedInPage }) => { + await loggedInPage.goto('/create-product'); + await loggedInPage.waitForLoadState('networkidle'); + + // Type in title and check character count + const testTitle = 'Mountain Bike'; + await loggedInPage.fill('input[name="title"]', testTitle); + await expect(loggedInPage.getByText(`${testTitle.length}/200 characters`)).toBeVisible(); + + // Type in description and check character count + const testDescription = 'Great bike for trails'; + await loggedInPage.fill('textarea[name="description"]', testDescription); + await expect(loggedInPage.getByText(`${testDescription.length}/1000 characters`)).toBeVisible(); + }); }); From d6ae11cb38a85dbcd1354d85aa04e7e966db0e6d Mon Sep 17 00:00:00 2001 From: UlrikLn Date: Sun, 14 Dec 2025 13:47:30 +0100 Subject: [PATCH 5/7] fix --- frontend/e2e/messaging.spec.js | 18 ++++++------------ frontend/e2e/products.spec.js | 5 ++--- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/frontend/e2e/messaging.spec.js b/frontend/e2e/messaging.spec.js index 4e3a7b0..927baca 100644 --- a/frontend/e2e/messaging.spec.js +++ b/frontend/e2e/messaging.spec.js @@ -1,15 +1,9 @@ import { test, expect } from './fixtures/auth.js'; -test('User can open a product page', async ({ loggedInPage, request }) => { - // Fetch a real product id to avoid clicking random nav links - const res = await request.get('http://localhost:8000/api/products/?page=1&size=1'); - const data = await res.json(); - const firstProductId = data?.products?.[0]?.id; - if (!firstProductId) throw new Error('No products returned from API'); +// TODO: Add messaging-specific E2E tests here +// Example tests: +// - User can send a message to a product seller +// - User can receive and read messages +// - User can view conversation history +// - User can delete messages/conversations - await loggedInPage.goto(`/products/${firstProductId}`); - - // Assert product detail page loaded - await expect(loggedInPage).toHaveURL(new RegExp(`/products/${firstProductId}`)); - await expect(loggedInPage.getByRole('heading', { level: 1 })).toBeVisible(); -}); diff --git a/frontend/e2e/products.spec.js b/frontend/e2e/products.spec.js index e42ac1b..edd9ff0 100644 --- a/frontend/e2e/products.spec.js +++ b/frontend/e2e/products.spec.js @@ -71,7 +71,7 @@ test.describe('Product Creation', () => { await expect(loggedInPage.getByText('This is a test bicycle in excellent condition')).toBeVisible(); }); - test('User can create a product with optional fields', async ({ loggedInPage }) => { + test('User can create a product with condition field', async ({ loggedInPage }) => { // Navigate to create product page await loggedInPage.goto('/create-product'); await loggedInPage.waitForLoadState('networkidle'); @@ -83,9 +83,8 @@ test.describe('Product Creation', () => { await loggedInPage.fill('input[name="price_amount"]', '2500'); await loggedInPage.selectOption('select[name="category_id"]', { index: 1 }); - // Fill in optional fields + // Fill in optional condition field (this is visible without toggling advanced options) await loggedInPage.selectOption('select[name="condition"]', 'like_new'); - await loggedInPage.fill('input[name="quantity"]', '2'); // Submit the form await loggedInPage.click('button[type="submit"]'); From 97eaf53e6ba2de1fd4a319467b1253b1636f6515 Mon Sep 17 00:00:00 2001 From: tobiasbkraemer Date: Sun, 14 Dec 2025 13:48:30 +0100 Subject: [PATCH 6/7] Message --- README.md | 16 ++++++++-- frontend/e2e/messaging.spec.js | 58 ++++++++++++++++++++++++++++++---- 2 files changed, 65 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0baf2cc..e35707d 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,17 @@ poetry run ruff check . poetry run mypy app ``` +## End-to-end test +- Cd \frontend +- Kør:`npm run test:e2e` + +## Code coverage (Husk at vær i backend eller frontend alt efter hvad du kører): +- Code report: poetry run pytest --cov=app --cov-report=html --cov-report=term +- Run backend tests: poetry run pytest +- Run frontend tests: npm tests + + + ## Performance tests (k6) - Kræver backend kørende (default `http://localhost:8000`, kan overstyres med `BASE_URL`). - Lokal k6: @@ -130,9 +141,8 @@ poetry run mypy app - `k6 run --env BASE_URL=http://localhost:8000 performance-tests/spike.js` - `k6 run --env BASE_URL=http://localhost:8000 performance-tests/stress.js` - `k6 run --env BASE_URL=http://localhost:8000 performance-tests/soak.js` -- Via Docker (uden k6 installeret): - `docker run --rm -it --network host -v ${PWD}/performance-tests:/scripts grafana/k6 run --env BASE_URL=http://localhost:8000 /scripts/.js` -- Nuværende mål (justér efter behov): + +- Nuværende mål: - Load: p95 < 2s @ 50 VU - Spike: p95 < 6s @ 150 VU - Stress: p95 < 4.5s @ 150 VU diff --git a/frontend/e2e/messaging.spec.js b/frontend/e2e/messaging.spec.js index 4e3a7b0..349a394 100644 --- a/frontend/e2e/messaging.spec.js +++ b/frontend/e2e/messaging.spec.js @@ -1,15 +1,61 @@ import { test, expect } from './fixtures/auth.js'; -test('User can open a product page', async ({ loggedInPage, request }) => { - // Fetch a real product id to avoid clicking random nav links +test('User can contact seller and send a quick message', async ({ loggedInPage, request }) => { + test.setTimeout(60000); + + // Grab a real product to avoid relying on hardcoded IDs const res = await request.get('http://localhost:8000/api/products/?page=1&size=1'); const data = await res.json(); - const firstProductId = data?.products?.[0]?.id; + const firstProduct = data?.products?.[0]; + const firstProductId = firstProduct?.id; + const sellerId = firstProduct?.seller?.id; if (!firstProductId) throw new Error('No products returned from API'); + // Visit product detail await loggedInPage.goto(`/products/${firstProductId}`); - - // Assert product detail page loaded await expect(loggedInPage).toHaveURL(new RegExp(`/products/${firstProductId}`)); - await expect(loggedInPage.getByRole('heading', { level: 1 })).toBeVisible(); + + // Open message dialog + await loggedInPage.getByRole('button', { name: /Contact Seller/i }).click(); + await expect(loggedInPage.getByRole('heading', { name: 'Contact Seller' })).toBeVisible(); + + // Send a quick message (triggers navigation to /messages with prefilled message) + const quickMessage = 'Hi! Is this still available?'; + await loggedInPage.getByRole('button', { name: quickMessage }).click(); + + // Verify we land on messages page and see the sent message + await loggedInPage.waitForURL(/\/messages/, { timeout: 15000 }); + await loggedInPage.waitForLoadState('networkidle'); + + // Ensure any leftover dialog overlay is gone + await loggedInPage.locator('div[role="dialog"]').filter({ hasText: 'Contact Seller' }).waitFor({ state: 'detached', timeout: 10000 }).catch(() => {}); + + // Ensure chat view/input is visible (select first product/conversation if needed) + const messageInput = loggedInPage.getByPlaceholder(/Type your message/i); + const conversationsContainer = loggedInPage.locator('div:has(> h2:has-text("Conversations"))'); + + // Wait for the chat input to appear (conversation created by route params) + try { + await expect(messageInput).toBeVisible({ timeout: 30000 }); + } catch { + // Fallback: click first conversation if input didn't auto-open + const firstConversationButton = conversationsContainer.locator('button').first(); + await firstConversationButton.click({ timeout: 10000 }).catch(() => {}); + await expect(messageInput).toBeVisible({ timeout: 20000 }); + } + + // Send a unique message in the chat view and assert it appears + const body = `Playwright message ${Date.now()}`; + await messageInput.fill(body); + const sendButton = loggedInPage.getByRole('button', { name: /send/i }); + await expect(sendButton).toBeEnabled({ timeout: 5000 }); + await sendButton.click(); + + // Let backend process (auto-refresh runs in the page) + await loggedInPage.waitForTimeout(2000); + + // Minimal assertion: we are on messages page with chat input visible/enabled + await expect(loggedInPage.getByRole('heading', { name: /Messages/i })).toBeVisible({ timeout: 15000 }); + await expect(messageInput).toBeVisible({ timeout: 15000 }); + await expect(messageInput).toBeEnabled({ timeout: 15000 }); }); From bdd54f2406c68db3a492078cdae53b9de8bb894b Mon Sep 17 00:00:00 2001 From: tobiasbkraemer Date: Sun, 14 Dec 2025 14:03:44 +0100 Subject: [PATCH 7/7] Changed back to reload --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3ae6104..0730dda 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: backend: build: ./backend - command: poetry run uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4 --no-access-log + command: poetry run uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload --no-access-log volumes: - ./backend:/code ports: