From 4fa2aa91bfae992a5e6bc3e57853b591344a33c4 Mon Sep 17 00:00:00 2001 From: Antonis Date: Sun, 5 Oct 2025 16:04:46 +0300 Subject: [PATCH] Fix E2E test breakage (#380) * Add logging placeholder functions, fix notes.js uids * Add UIDs to inbox items. Includes migration. * Add UID to users. * Add project UIDs usage. * Add script that runs specific E2E test(s) * Only run Chromium E2E tests by default. * Fix breaking E2E tests * fixup! Fix breaking E2E tests --------- Co-authored-by: antanst <> Co-authored-by: Chris Veleris --- .gitignore | 1 + backend/jest.config.js | 2 +- .../20250925000001-add-uid-to-users.js | 66 ++++++++++ backend/models/user.js | 7 ++ backend/routes/auth.js | 22 ++-- backend/routes/projects.js | 47 +++----- backend/routes/users.js | 4 +- backend/tests/integration/auth.test.js | 6 +- backend/tests/integration/projects.test.js | 22 ++-- backend/tests/integration/users.test.js | 5 +- e2e/bin/run-single-test.sh | 113 ++++++++++++++++++ e2e/playwright.config.ts | 4 +- e2e/tests/inbox.spec.ts | 18 ++- frontend/Layout.tsx | 10 +- frontend/components/Inbox/InboxItems.tsx | 2 +- .../components/Profile/ProfileSettings.tsx | 4 +- .../components/Project/ProjectDetails.tsx | 8 +- frontend/components/Project/ProjectModal.tsx | 10 +- frontend/components/Projects.tsx | 16 +-- frontend/components/Shared/NoteCard.tsx | 2 +- frontend/entities/User.ts | 2 +- frontend/utils/projectsService.ts | 14 +-- package.json | 1 + 23 files changed, 278 insertions(+), 108 deletions(-) create mode 100644 backend/migrations/20250925000001-add-uid-to-users.js create mode 100755 e2e/bin/run-single-test.sh diff --git a/.gitignore b/.gitignore index d3a3665..823da3b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ certs/ .DS_Store .cursor +AGENTS.md CLAUDE.local.md .byebug_history diff --git a/backend/jest.config.js b/backend/jest.config.js index 01affd3..5c70546 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -13,7 +13,7 @@ module.exports = { ], coverageDirectory: 'coverage', coverageReporters: ['text', 'lcov', 'html'], - verbose: true, + verbose: false, forceExit: true, clearMocks: true, resetMocks: true, diff --git a/backend/migrations/20250925000001-add-uid-to-users.js b/backend/migrations/20250925000001-add-uid-to-users.js new file mode 100644 index 0000000..325b71b --- /dev/null +++ b/backend/migrations/20250925000001-add-uid-to-users.js @@ -0,0 +1,66 @@ +'use strict'; + +const { uid } = require('../utils/uid'); +const { safeAddColumns, safeAddIndex } = require('../utils/migration-utils'); + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.sequelize.query('PRAGMA foreign_keys = OFF'); + + try { + await safeAddColumns(queryInterface, 'users', [ + { + name: 'uid', + definition: { + type: Sequelize.STRING, + allowNull: true, + }, + }, + ]); + + const users = await queryInterface.sequelize.query( + 'SELECT id FROM users WHERE uid IS NULL', + { type: Sequelize.QueryTypes.SELECT } + ); + + for (const user of users) { + const uniqueId = uid(); + await queryInterface.sequelize.query( + 'UPDATE users SET uid = ? WHERE id = ?', + { + replacements: [uniqueId, user.id], + type: Sequelize.QueryTypes.UPDATE, + } + ); + } + + await queryInterface.changeColumn('users', 'uid', { + type: Sequelize.STRING, + allowNull: false, + unique: true, + }); + + await safeAddIndex(queryInterface, 'users', ['uid'], { + unique: true, + name: 'users_uid_unique_index', + }); + } finally { + await queryInterface.sequelize.query('PRAGMA foreign_keys = ON'); + } + }, + + async down(queryInterface) { + try { + await queryInterface.removeIndex('users', 'users_uid_unique_index'); + } catch (error) { + console.log('users_uid_unique_index not found, skipping removal'); + } + + try { + await queryInterface.removeColumn('users', 'uid'); + } catch (error) { + console.log('Error removing uid column from users:', error.message); + } + }, +}; diff --git a/backend/models/user.js b/backend/models/user.js index 128d502..fbe3c3b 100644 --- a/backend/models/user.js +++ b/backend/models/user.js @@ -1,5 +1,6 @@ const { DataTypes } = require('sequelize'); const bcrypt = require('bcrypt'); +const { uid } = require('../utils/uid'); module.exports = (sequelize) => { const User = sequelize.define( @@ -10,6 +11,12 @@ module.exports = (sequelize) => { primaryKey: true, autoIncrement: true, }, + uid: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + defaultValue: uid, + }, name: { type: DataTypes.STRING, allowNull: true, diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 1a231d6..9fd4442 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -12,17 +12,17 @@ router.get('/version', (req, res) => { router.get('/current_user', async (req, res) => { try { if (req.session && req.session.userId) { - const user = await User.findByPk(req.session.userId); + const user = await User.findByPk(req.session.userId, { + attributes: [ + 'uid', + 'email', + 'language', + 'appearance', + 'timezone', + ], + }); if (user) { - return res.json({ - user: { - id: user.id, - email: user.email, - language: user.language, - appearance: user.appearance, - timezone: user.timezone, - }, - }); + return res.json({ user }); } } @@ -66,7 +66,7 @@ router.post('/login', async (req, res) => { res.json({ user: { - id: user.id, + uid: user.uid, email: user.email, language: user.language, appearance: user.appearance, diff --git a/backend/routes/projects.js b/backend/routes/projects.js index 4b6839a..e757dd2 100644 --- a/backend/routes/projects.js +++ b/backend/routes/projects.js @@ -9,6 +9,7 @@ const { Op } = require('sequelize'); const { extractUidFromSlug } = require('../utils/slug-utils'); const { validateTagName } = require('../services/tagsService'); const { uid } = require('../utils/uid'); +const { logError } = require('../services/logService'); const router = express.Router(); // Helper function to safely format dates @@ -113,10 +114,6 @@ async function updateProjectTags(project, tagsData, userId) { // POST /api/upload/project-image router.post('/upload/project-image', upload.single('image'), (req, res) => { try { - if (!req.session || !req.session.userId) { - return res.status(401).json({ error: 'Authentication required' }); - } - if (!req.file) { return res.status(400).json({ error: 'No image file provided' }); } @@ -125,7 +122,7 @@ router.post('/upload/project-image', upload.single('image'), (req, res) => { const imageUrl = `/api/uploads/projects/${req.file.filename}`; res.json({ imageUrl }); } catch (error) { - console.error('Error uploading image:', error); + logError('Error uploading image:', error); res.status(500).json({ error: 'Failed to upload image' }); } }); @@ -251,7 +248,7 @@ router.get('/projects', async (req, res) => { }); } } catch (error) { - console.error('Error fetching projects:', error); + logError('Error fetching projects:', error); res.status(500).json({ error: 'Internal server error' }); } }); @@ -259,10 +256,6 @@ router.get('/projects', async (req, res) => { // GET /api/project/:uidSlug (UID-slug format only) router.get('/project/:uidSlug', async (req, res) => { try { - if (!req.session || !req.session.userId) { - return res.status(401).json({ error: 'Authentication required' }); - } - // Extract UID from the slug (part before first hyphen) const uidPart = req.params.uidSlug.split('-')[0]; @@ -379,7 +372,7 @@ router.get('/project/:uidSlug', async (req, res) => { res.json(result); } catch (error) { - console.error('Error fetching project:', error); + logError('Error fetching project:', error); res.status(500).json({ error: 'Internal server error' }); } }); @@ -387,10 +380,6 @@ router.get('/project/:uidSlug', async (req, res) => { // POST /api/project router.post('/project', async (req, res) => { try { - if (!req.session || !req.session.userId) { - return res.status(401).json({ error: 'Authentication required' }); - } - const { name, description, @@ -445,7 +434,7 @@ router.post('/project', async (req, res) => { due_date_at: formatDate(project.due_date_at), }); } catch (error) { - console.error('Error creating project:', error); + logError('Error creating project:', error); res.status(400).json({ error: 'There was a problem creating the project.', details: error.errors @@ -455,15 +444,11 @@ router.post('/project', async (req, res) => { } }); -// PATCH /api/project/:id -router.patch('/project/:id', async (req, res) => { +// PATCH /api/project/:uid +router.patch('/project/:uid', async (req, res) => { try { - if (!req.session || !req.session.userId) { - return res.status(401).json({ error: 'Authentication required' }); - } - const project = await Project.findOne({ - where: { id: req.params.id, user_id: req.session.userId }, + where: { uid: req.params.uid, user_id: req.session.userId }, }); if (!project) { @@ -519,7 +504,7 @@ router.patch('/project/:id', async (req, res) => { due_date_at: formatDate(projectWithAssociations.due_date_at), }); } catch (error) { - console.error('Error updating project:', error); + logError('Error updating project:', error); res.status(400).json({ error: 'There was a problem updating the project.', details: error.errors @@ -529,15 +514,11 @@ router.patch('/project/:id', async (req, res) => { } }); -// DELETE /api/project/:id -router.delete('/project/:id', async (req, res) => { +// DELETE /api/project/:uid +router.delete('/project/:uid', async (req, res) => { try { - if (!req.session || !req.session.userId) { - return res.status(401).json({ error: 'Authentication required' }); - } - const project = await Project.findOne({ - where: { id: req.params.id, user_id: req.session.userId }, + where: { uid: req.params.uid, user_id: req.session.userId }, }); if (!project) { @@ -555,7 +536,7 @@ router.delete('/project/:id', async (req, res) => { { project_id: null }, { where: { - project_id: req.params.id, + project_id: project.id, user_id: req.session.userId, }, transaction, @@ -574,7 +555,7 @@ router.delete('/project/:id', async (req, res) => { res.json({ message: 'Project successfully deleted' }); } catch (error) { - console.error('Error deleting project:', error); + logError('Error deleting project:', error); res.status(400).json({ error: 'There was a problem deleting the project.', }); diff --git a/backend/routes/users.js b/backend/routes/users.js index 5b97fbf..7edb9d3 100644 --- a/backend/routes/users.js +++ b/backend/routes/users.js @@ -23,7 +23,7 @@ router.get('/profile', async (req, res) => { const user = await User.findByPk(req.session.userId, { attributes: [ - 'id', + 'uid', 'email', 'appearance', 'language', @@ -172,7 +172,7 @@ router.patch('/profile', async (req, res) => { // Return updated user with limited fields const updatedUser = await User.findByPk(user.id, { attributes: [ - 'id', + 'uid', 'email', 'appearance', 'language', diff --git a/backend/tests/integration/auth.test.js b/backend/tests/integration/auth.test.js index 43e19e0..76ed618 100644 --- a/backend/tests/integration/auth.test.js +++ b/backend/tests/integration/auth.test.js @@ -22,7 +22,8 @@ describe('Auth Routes', () => { expect(response.status).toBe(200); expect(response.body.user).toBeDefined(); expect(response.body.user.email).toBe('test@example.com'); - expect(response.body.user.id).toBe(user.id); + expect(response.body.user.uid).toBe(user.uid); + expect(response.body.user).not.toHaveProperty('id'); expect(response.body.user.language).toBe('en'); expect(response.body.user.appearance).toBe('light'); expect(response.body.user.timezone).toBe('UTC'); @@ -91,7 +92,8 @@ describe('Auth Routes', () => { expect(response.status).toBe(200); expect(response.body.user).toBeDefined(); expect(response.body.user.email).toBe('test@example.com'); - expect(response.body.user.id).toBe(user.id); + expect(response.body.user.uid).toBe(user.uid); + expect(response.body.user).not.toHaveProperty('id'); }); it('should return null user when not logged in', async () => { diff --git a/backend/tests/integration/projects.test.js b/backend/tests/integration/projects.test.js index 43b3308..f7c12f3 100644 --- a/backend/tests/integration/projects.test.js +++ b/backend/tests/integration/projects.test.js @@ -208,7 +208,7 @@ describe('Projects Routes', () => { }; const response = await agent - .patch(`/api/project/${project.id}`) + .patch(`/api/project/${project.uid}`) .send(updateData); expect(response.status).toBe(200); @@ -220,7 +220,7 @@ describe('Projects Routes', () => { it('should return 404 for non-existent project', async () => { const response = await agent - .patch('/api/project/999999') + .patch('/api/project/nonexistentuid') .send({ name: 'Updated' }); expect(response.status).toBe(404); @@ -240,7 +240,7 @@ describe('Projects Routes', () => { }); const response = await agent - .patch(`/api/project/${otherProject.id}`) + .patch(`/api/project/${otherProject.uid}`) .send({ name: 'Updated' }); expect(response.status).toBe(404); @@ -249,7 +249,7 @@ describe('Projects Routes', () => { it('should require authentication', async () => { const response = await request(app) - .patch(`/api/project/${project.id}`) + .patch(`/api/project/${project.uid}`) .send({ name: 'Updated' }); expect(response.status).toBe(401); @@ -268,7 +268,7 @@ describe('Projects Routes', () => { }); it('should delete project', async () => { - const response = await agent.delete(`/api/project/${project.id}`); + const response = await agent.delete(`/api/project/${project.uid}`); expect(response.status).toBe(200); expect(response.body.message).toBe('Project successfully deleted'); @@ -279,7 +279,7 @@ describe('Projects Routes', () => { }); it('should return 404 for non-existent project', async () => { - const response = await agent.delete('/api/project/999999'); + const response = await agent.delete('/api/project/nonexistentuid'); expect(response.status).toBe(404); expect(response.body.error).toBe('Project not found.'); @@ -298,7 +298,7 @@ describe('Projects Routes', () => { }); const response = await agent.delete( - `/api/project/${otherProject.id}` + `/api/project/${otherProject.uid}` ); expect(response.status).toBe(404); @@ -307,7 +307,7 @@ describe('Projects Routes', () => { it('should require authentication', async () => { const response = await request(app).delete( - `/api/project/${project.id}` + `/api/project/${project.uid}` ); expect(response.status).toBe(401); @@ -331,7 +331,7 @@ describe('Projects Routes', () => { }); // Delete the project - const response = await agent.delete(`/api/project/${project.id}`); + const response = await agent.delete(`/api/project/${project.uid}`); expect(response.status).toBe(200); expect(response.body.message).toBe('Project successfully deleted'); @@ -363,7 +363,7 @@ describe('Projects Routes', () => { }); // Delete the project - const response = await agent.delete(`/api/project/${project.id}`); + const response = await agent.delete(`/api/project/${project.uid}`); expect(response.status).toBe(200); expect(response.body.message).toBe('Project successfully deleted'); @@ -403,7 +403,7 @@ describe('Projects Routes', () => { }); // Delete the project - const response = await agent.delete(`/api/project/${project.id}`); + const response = await agent.delete(`/api/project/${project.uid}`); expect(response.status).toBe(200); expect(response.body.message).toBe('Project successfully deleted'); diff --git a/backend/tests/integration/users.test.js b/backend/tests/integration/users.test.js index 52296f9..65acc61 100644 --- a/backend/tests/integration/users.test.js +++ b/backend/tests/integration/users.test.js @@ -24,7 +24,8 @@ describe('Users Routes', () => { const response = await agent.get('/api/profile'); expect(response.status).toBe(200); - expect(response.body.id).toBe(user.id); + expect(response.body.uid).toBe(user.uid); + expect(response.body).not.toHaveProperty('id'); expect(response.body.email).toBe(user.email); expect(response.body).toHaveProperty('appearance'); expect(response.body).toHaveProperty('language'); @@ -66,6 +67,8 @@ describe('Users Routes', () => { const response = await agent.patch('/api/profile').send(updateData); expect(response.status).toBe(200); + expect(response.body.uid).toBe(user.uid); + expect(response.body).not.toHaveProperty('id'); expect(response.body.appearance).toBe(updateData.appearance); expect(response.body.language).toBe(updateData.language); expect(response.body.timezone).toBe(updateData.timezone); diff --git a/e2e/bin/run-single-test.sh b/e2e/bin/run-single-test.sh new file mode 100755 index 0000000..95936a9 --- /dev/null +++ b/e2e/bin/run-single-test.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Usage: ./run-single-test.sh "test name pattern" [browser] +# Example: ./run-single-test.sh "delete an existing project" firefox + +if [ $# -lt 1 ]; then + echo "Usage: $0 [browser]" + echo "Example: $0 'delete an existing project' firefox" + exit 1 +fi + +TEST_PATTERN="$1" +BROWSER="${2:-Chromium}" + +# Config +APP_URL_DEFAULT="http://localhost:8080" +BACKEND_URL="http://localhost:3002" +BACKEND_HEALTH="${BACKEND_URL}/api/health" +FRONTEND_URL="${APP_URL:-$APP_URL_DEFAULT}" + +# Colors +red() { printf "\033[31m%s\033[0m\n" "$*"; } +green() { printf "\033[32m%s\033[0m\n" "$*"; } +yellow() { printf "\033[33m%s\033[0m\n" "$*"; } + +# Setup paths +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +E2E_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +ROOT_DIR="$(cd "$E2E_DIR/.." && pwd)" + +cd "$E2E_DIR" +if [ ! -f package.json ]; then + red "e2e/package.json not found" + exit 1 +fi + +# Install e2e deps if needed +if [ ! -d node_modules ]; then + yellow "Installing e2e dependencies..." + npm ci +fi + +# Start backend and frontend +cd "$ROOT_DIR" + +yellow "Starting backend..." +TUDUDI_USER_EMAIL="${E2E_EMAIL:-test@tududi.com}" \ +TUDUDI_USER_PASSWORD="${E2E_PASSWORD:-password123}" \ +SEQUELIZE_LOGGING=false \ +npm run backend:start >/dev/null 2>&1 & +BACKEND_PID=$! + +cleanup() { + yellow "Stopping background processes..." + # Kill by PIDs + if [ -n "${FRONTEND_PID:-}" ]; then kill -TERM -$FRONTEND_PID >/dev/null 2>&1 || true; fi + if [ -n "${BACKEND_PID:-}" ]; then kill -TERM -$BACKEND_PID >/dev/null 2>&1 || true; fi + + # Kill by ports (best-effort) + if command -v lsof >/dev/null 2>&1; then + FRONTEND_PIDS_KILL=$(lsof -ti tcp:8080 || true) + BACKEND_PIDS_KILL=$(lsof -ti tcp:3002 || true) + if [ -n "${FRONTEND_PIDS_KILL:-}" ]; then kill ${FRONTEND_PIDS_KILL} >/dev/null 2>&1 || true; fi + if [ -n "${BACKEND_PIDS_KILL:-}" ]; then kill ${BACKEND_PIDS_KILL} >/dev/null 2>&1 || true; fi + fi + + # Fallback direct kill + if [ -n "${FRONTEND_PID:-}" ] && ps -p $FRONTEND_PID >/dev/null 2>&1; then kill $FRONTEND_PID || true; fi + if [ -n "${BACKEND_PID:-}" ] && ps -p $BACKEND_PID >/dev/null 2>&1; then kill $BACKEND_PID || true; fi +} +trap cleanup EXIT INT TERM + +# Wait for backend health +yellow "Waiting for backend to be ready at ${BACKEND_HEALTH}..." +for i in {1..60}; do + if curl -sf "$BACKEND_HEALTH" >/dev/null; then + green "Backend is ready" + break + fi + sleep 1 + if [ "$i" -eq 60 ]; then + red "Backend did not become ready in time" + exit 1 + fi +done + +yellow "Starting frontend dev server..." +npm run frontend:dev >/dev/null 2>&1 & +FRONTEND_PID=$! + +# Wait for frontend +yellow "Waiting for frontend at ${FRONTEND_URL}..." +for i in {1..60}; do + if curl -sf "$FRONTEND_URL" >/dev/null; then + green "Frontend is ready" + break + fi + sleep 1 + if [ "$i" -eq 60 ]; then + red "Frontend did not become ready in time" + exit 1 + fi +done + +# Run tests +cd "$E2E_DIR" + +yellow "Running Playwright tests matching: ${TEST_PATTERN} on ${BROWSER}..." +APP_URL="$FRONTEND_URL" \ +E2E_EMAIL="${E2E_EMAIL:-test@tududi.com}" \ +E2E_PASSWORD="${E2E_PASSWORD:-password123}" \ +npx playwright test --grep "$TEST_PATTERN" --project="$BROWSER" \ No newline at end of file diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 4eb16a3..db15069 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -22,7 +22,7 @@ export default defineConfig({ }, projects: [ { name: 'Chromium', use: { ...devices['Desktop Chrome'] } }, - { name: 'Firefox', use: { ...devices['Desktop Firefox'] } }, - { name: 'WebKit', use: { ...devices['Desktop Safari'] } }, + // { name: 'Firefox', use: { ...devices['Desktop Firefox'] } }, + // { name: 'WebKit', use: { ...devices['Desktop Safari'] } }, ], }); diff --git a/e2e/tests/inbox.spec.ts b/e2e/tests/inbox.spec.ts index 3c5bdee..9d4b543 100644 --- a/e2e/tests/inbox.spec.ts +++ b/e2e/tests/inbox.spec.ts @@ -176,8 +176,8 @@ test('user can create project from inbox item', async ({ page, baseURL }) => { const projectNameInput = page.locator('input[name="name"], input[placeholder*="project" i], input[placeholder*="name" i]').first(); await expect(projectNameInput).toHaveValue(testContent); - // Save the project - Find submit button by looking for buttons in form context, force click through backdrop - await page.locator('form button[type="submit"], button:has-text("Save"), button:has-text("Create")').first().click({ force: true }); + // Save the project - Use the specific test ID + await page.locator('[data-testid="project-save-button"]').click(); // Wait for success message or modal to close await expect(page.locator('input[name="name"], input[placeholder*="project" i], input[placeholder*="name" i]')).not.toBeVisible({ timeout: 10000 }); @@ -191,16 +191,12 @@ test('user can create project from inbox item', async ({ page, baseURL }) => { // Navigate to projects page to verify the project was created there await page.goto(appUrl + '/projects'); await expect(page).toHaveURL(/\/projects$/); - - // Wait a moment for the page to load, then check if project exists (more lenient check) + + // Wait a moment for the page to load await page.waitForTimeout(2000); - const projectExists = await page.locator('*').filter({ hasText: testContent }).count() > 0; - if (!projectExists) { - // If exact match fails, just verify we're on projects page and there are projects - await expect(page.locator('h1, h2, h3').filter({ hasText: /projects/i }).first()).toBeVisible(); - } else { - await expect(page.locator('*').filter({ hasText: testContent })).toBeVisible(); - } + + // Verify the created project appears - use a more specific selector + await expect(page.getByRole('link', { name: new RegExp(testContent) }).first()).toBeVisible({ timeout: 10000 }); }); test('user can create note from inbox item', async ({ page, baseURL }) => { diff --git a/frontend/Layout.tsx b/frontend/Layout.tsx index 366cbdf..8f2d521 100644 --- a/frontend/Layout.tsx +++ b/frontend/Layout.tsx @@ -234,8 +234,8 @@ const Layout: React.FC = ({ const handleSaveProject = async (projectData: Project) => { try { - if (projectData.id) { - await updateProject(projectData.id, projectData); + if (projectData.uid) { + await updateProject(projectData.uid, projectData); } else { await createProject(projectData); } @@ -474,12 +474,12 @@ const Layout: React.FC = ({ isOpen={isProjectModalOpen} onClose={closeProjectModal} onSave={handleSaveProject} - onDelete={async (projectId) => { + onDelete={async (projectUid) => { try { const { deleteProject } = await import( './utils/projectsService' ); - await deleteProject(projectId); + await deleteProject(projectUid); // Update global projects store const currentProjects = @@ -488,7 +488,7 @@ const Layout: React.FC = ({ .getState() .projectsStore.setProjects( currentProjects.filter( - (p) => p.id !== projectId + (p) => p.uid !== projectUid ) ); diff --git a/frontend/components/Inbox/InboxItems.tsx b/frontend/components/Inbox/InboxItems.tsx index ce8aa6e..bc47cf1 100644 --- a/frontend/components/Inbox/InboxItems.tsx +++ b/frontend/components/Inbox/InboxItems.tsx @@ -347,7 +347,7 @@ const InboxItems: React.FC = () => { setCurrentConversionItemUid(null); } - setIsProjectModalOpen(false); + // Don't set isProjectModalOpen here - the modal handles its own closing via handleClose() } catch (error) { console.error('Failed to create project:', error); showErrorToast(t('project.createError')); diff --git a/frontend/components/Profile/ProfileSettings.tsx b/frontend/components/Profile/ProfileSettings.tsx index b8a805f..fce7412 100644 --- a/frontend/components/Profile/ProfileSettings.tsx +++ b/frontend/components/Profile/ProfileSettings.tsx @@ -32,13 +32,13 @@ import FirstDayOfWeekDropdown from '../Shared/FirstDayOfWeekDropdown'; import { getLocaleFirstDayOfWeek } from '../../utils/profileService'; interface ProfileSettingsProps { - currentUser: { id: number; email: string }; + currentUser: { uid: string; email: string }; isDarkMode?: boolean; toggleDarkMode?: () => void; } interface Profile { - id: number; + uid: string; email: string; appearance: 'light' | 'dark'; language: string; diff --git a/frontend/components/Project/ProjectDetails.tsx b/frontend/components/Project/ProjectDetails.tsx index 9db314e..55b6886 100644 --- a/frontend/components/Project/ProjectDetails.tsx +++ b/frontend/components/Project/ProjectDetails.tsx @@ -408,13 +408,13 @@ const ProjectDetails: React.FC = () => { }, [openModal]); const handleSaveProject = async (updatedProject: Project) => { - if (!updatedProject.id) { + if (!updatedProject.uid) { return; } try { const savedProject = await updateProject( - updatedProject.id, + updatedProject.uid, updatedProject ); // Merge the saved project with existing project to preserve area data @@ -510,12 +510,12 @@ const ProjectDetails: React.FC = () => { }; const handleDeleteProject = async () => { - if (!project?.id) { + if (!project?.uid) { return; } try { - await deleteProject(project.id); + await deleteProject(project.uid); navigate('/projects'); } catch { // Error deleting project - silently handled diff --git a/frontend/components/Project/ProjectModal.tsx b/frontend/components/Project/ProjectModal.tsx index 191c6e5..e6ece15 100644 --- a/frontend/components/Project/ProjectModal.tsx +++ b/frontend/components/Project/ProjectModal.tsx @@ -26,7 +26,7 @@ interface ProjectModalProps { isOpen: boolean; onClose: () => void; onSave: (project: Project) => void; - onDelete?: (projectId: number) => Promise; + onDelete?: (projectUid: string) => Promise; project?: Project; areas: Area[]; } @@ -345,8 +345,8 @@ const ProjectModal: React.FC = ({ tags: tags.map((name) => ({ name })), }; - // Save the project - onSave(projectData); + // Save the project and wait for it to complete + await onSave(projectData); showSuccessToast( project @@ -366,9 +366,9 @@ const ProjectModal: React.FC = ({ }; const handleDeleteConfirm = async () => { - if (project && project.id && onDelete) { + if (project && project.uid && onDelete) { try { - await onDelete(project.id); + await onDelete(project.uid); showSuccessToast(t('success.projectDeleted')); setShowConfirmDialog(false); handleClose(); diff --git a/frontend/components/Projects.tsx b/frontend/components/Projects.tsx index d417ea4..8742c46 100644 --- a/frontend/components/Projects.tsx +++ b/frontend/components/Projects.tsx @@ -163,8 +163,8 @@ const Projects: React.FC = () => { const handleSaveProject = async (project: Project) => { setProjectsLoading(true); try { - if (project.id) { - await updateProject(project.id, project); + if (project.uid) { + await updateProject(project.uid, project); } else { await createProject(project); } @@ -197,9 +197,9 @@ const Projects: React.FC = () => { if (!projectToDelete) return; try { - if (projectToDelete.id !== undefined) { + if (projectToDelete.uid !== undefined) { setProjectsLoading(true); - await deleteProject(projectToDelete.id); + await deleteProject(projectToDelete.uid); // Update global state const projectsData = await fetchProjects( @@ -208,7 +208,7 @@ const Projects: React.FC = () => { ); setProjects(projectsData); } else { - console.error('Cannot delete project: ID is undefined.'); + console.error('Cannot delete project: UID is undefined.'); } } catch (error) { console.error('Error deleting project:', error); @@ -495,13 +495,13 @@ const Projects: React.FC = () => { setModalState({ isOpen: false, projectToEdit: null }); }} onSave={handleSaveProject} - onDelete={async (projectId) => { + onDelete={async (projectUid) => { try { - await deleteProject(projectId); + await deleteProject(projectUid); // Update both local and global state const updatedProjects = projects.filter( - (p: Project) => p.id !== projectId + (p: Project) => p.uid !== projectUid ); setProjects(updatedProjects); diff --git a/frontend/components/Shared/NoteCard.tsx b/frontend/components/Shared/NoteCard.tsx index 53415f4..8a0ca71 100644 --- a/frontend/components/Shared/NoteCard.tsx +++ b/frontend/components/Shared/NoteCard.tsx @@ -231,7 +231,7 @@ const NoteCard: React.FC = ({ setDropdownOpen(false); }} className="block px-4 py-2 text-sm text-red-500 dark:text-red-300 hover:bg-gray-100 dark:hover:bg-gray-600 w-full text-left rounded-b-md" - data-testid={`note-delete-${noteIdentifier}`} + data-testid={`note-delete-${note.uid}`} > {t('notes.delete', 'Delete')} diff --git a/frontend/entities/User.ts b/frontend/entities/User.ts index a9c607e..a5abca3 100644 --- a/frontend/entities/User.ts +++ b/frontend/entities/User.ts @@ -1,5 +1,5 @@ export interface User { - id: number; + uid: string; email: string; language: string; appearance: string; diff --git a/frontend/utils/projectsService.ts b/frontend/utils/projectsService.ts index bcbd0fb..48cb584 100644 --- a/frontend/utils/projectsService.ts +++ b/frontend/utils/projectsService.ts @@ -74,10 +74,10 @@ export const createProject = async ( }; export const updateProject = async ( - projectId: number, + projectUid: string, projectData: Partial ): Promise => { - const response = await fetch(`/api/project/${projectId}`, { + const response = await fetch(`/api/project/${projectUid}`, { method: 'PATCH', credentials: 'include', headers: { @@ -91,14 +91,14 @@ export const updateProject = async ( return await response.json(); }; -export const deleteProject = async (projectId: number): Promise => { - if (!projectId || projectId === null || projectId === undefined) { - throw new Error('Cannot delete project: Invalid project ID'); +export const deleteProject = async (projectUid: string): Promise => { + if (!projectUid || projectUid === null || projectUid === undefined) { + throw new Error('Cannot delete project: Invalid project UID'); } - console.log('Attempting to delete project with ID:', projectId); + console.log('Attempting to delete project with UID:', projectUid); - const response = await fetch(`/api/project/${projectId}`, { + const response = await fetch(`/api/project/${projectUid}`, { method: 'DELETE', credentials: 'include', headers: { diff --git a/package.json b/package.json index 0bab33c..d11e8c8 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "pre-push": "npm run lint:fix && npm run format:fix", "pre-release": "npm run lint:fix && npm run format:fix && npm run test && npm run test:ui", "test": "npm run backend:test", + "test:backend": "npm run backend:test", "test:ui": "bash e2e/bin/run-e2e.sh && echo \"Success!\"", "test:ui:headed": "cross-env E2E_MODE=headed E2E_SLOWMO=500 bash e2e/bin/run-e2e.sh", "test:watch": "npm run frontend:test:watch",