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 <chrisveleris@gmail.com>
This commit is contained in:
Antonis 2025-10-05 16:04:46 +03:00 committed by GitHub
parent 7651677b71
commit 4fa2aa91bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 278 additions and 108 deletions

1
.gitignore vendored
View file

@ -5,6 +5,7 @@
certs/
.DS_Store
.cursor
AGENTS.md
CLAUDE.local.md
.byebug_history

View file

@ -13,7 +13,7 @@ module.exports = {
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
verbose: true,
verbose: false,
forceExit: true,
clearMocks: true,
resetMocks: true,

View file

@ -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);
}
},
};

View file

@ -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,

View file

@ -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,

View file

@ -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.',
});

View file

@ -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',

View file

@ -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 () => {

View file

@ -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');

View file

@ -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);

113
e2e/bin/run-single-test.sh Executable file
View file

@ -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 <test-name-pattern> [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"

View file

@ -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'] } },
],
});

View file

@ -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 }) => {

View file

@ -234,8 +234,8 @@ const Layout: React.FC<LayoutProps> = ({
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<LayoutProps> = ({
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<LayoutProps> = ({
.getState()
.projectsStore.setProjects(
currentProjects.filter(
(p) => p.id !== projectId
(p) => p.uid !== projectUid
)
);

View file

@ -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'));

View file

@ -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;

View file

@ -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

View file

@ -26,7 +26,7 @@ interface ProjectModalProps {
isOpen: boolean;
onClose: () => void;
onSave: (project: Project) => void;
onDelete?: (projectId: number) => Promise<void>;
onDelete?: (projectUid: string) => Promise<void>;
project?: Project;
areas: Area[];
}
@ -345,8 +345,8 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
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<ProjectModalProps> = ({
};
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();

View file

@ -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);

View file

@ -231,7 +231,7 @@ const NoteCard: React.FC<NoteCardProps> = ({
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')}
</button>

View file

@ -1,5 +1,5 @@
export interface User {
id: number;
uid: string;
email: string;
language: string;
appearance: string;

View file

@ -74,10 +74,10 @@ export const createProject = async (
};
export const updateProject = async (
projectId: number,
projectUid: string,
projectData: Partial<Project>
): Promise<Project> => {
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<void> => {
if (!projectId || projectId === null || projectId === undefined) {
throw new Error('Cannot delete project: Invalid project ID');
export const deleteProject = async (projectUid: string): Promise<void> => {
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: {

View file

@ -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",