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:
parent
7651677b71
commit
4fa2aa91bf
23 changed files with 278 additions and 108 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -5,6 +5,7 @@
|
|||
certs/
|
||||
.DS_Store
|
||||
.cursor
|
||||
AGENTS.md
|
||||
CLAUDE.local.md
|
||||
|
||||
.byebug_history
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ module.exports = {
|
|||
],
|
||||
coverageDirectory: 'coverage',
|
||||
coverageReporters: ['text', 'lcov', 'html'],
|
||||
verbose: true,
|
||||
verbose: false,
|
||||
forceExit: true,
|
||||
clearMocks: true,
|
||||
resetMocks: true,
|
||||
|
|
|
|||
66
backend/migrations/20250925000001-add-uid-to-users.js
Normal file
66
backend/migrations/20250925000001-add-uid-to-users.js
Normal 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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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
113
e2e/bin/run-single-test.sh
Executable 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"
|
||||
|
|
@ -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'] } },
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export interface User {
|
||||
id: number;
|
||||
uid: string;
|
||||
email: string;
|
||||
language: string;
|
||||
appearance: string;
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue