Dev and Test run fixed. New Datepicker. Tests fixed. Message bookmarking fixed.
This commit is contained in:
26
package-lock.json
generated
26
package-lock.json
generated
@@ -29,6 +29,7 @@
|
|||||||
"@vitejs/plugin-react": "^5.0.0",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
"autoprefixer": "^10.4.22",
|
"autoprefixer": "^10.4.22",
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
|
"cross-env": "^10.1.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "~5.8.2",
|
"typescript": "~5.8.2",
|
||||||
@@ -411,6 +412,13 @@
|
|||||||
"react": ">=16.8.0"
|
"react": ">=16.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@epic-web/invariant": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||||
@@ -2427,6 +2435,24 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cross-env": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@epic-web/invariant": "^1.0.0",
|
||||||
|
"cross-spawn": "^7.0.6"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"cross-env": "dist/bin/cross-env.js",
|
||||||
|
"cross-env-shell": "dist/bin/cross-env-shell.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"prod:full": "npm-run-all --parallel preview server:prod",
|
"prod:full": "npm-run-all --parallel preview server:prod",
|
||||||
"server:prod": "npm run start:prod --prefix server",
|
"server:prod": "npm run start:prod --prefix server",
|
||||||
"server:test": "npm run start:test --prefix server",
|
"server:test": "npm run start:test --prefix server",
|
||||||
"test:full": "npm-run-all --parallel dev server:test"
|
"test:full": "cross-env BACKEND_PORT=3201 npm-run-all --parallel dev server:test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
@@ -36,6 +36,7 @@
|
|||||||
"@vitejs/plugin-react": "^5.0.0",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
"autoprefixer": "^10.4.22",
|
"autoprefixer": "^10.4.22",
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
|
"cross-env": "^10.1.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "~5.8.2",
|
"typescript": "~5.8.2",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default defineConfig({
|
|||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
reporter: 'html',
|
reporter: 'html',
|
||||||
use: {
|
use: {
|
||||||
baseURL: 'http://localhost:3000',
|
baseURL: 'http://localhost:5173',
|
||||||
trace: 'on-first-retry',
|
trace: 'on-first-retry',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
17
server/check_table.ts
Normal file
17
server/check_table.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import prisma from './src/lib/prisma';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
console.log('Checking SavedMessage table...');
|
||||||
|
const count = await prisma.savedMessage.count();
|
||||||
|
console.log(`SavedMessage table exists, count: ${count}`);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error accessing SavedMessage table:', error.message);
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -6,7 +6,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"start:prod": "node dist/index.js",
|
"start:prod": "node dist/index.js",
|
||||||
"start:dev": "cross-env APP_MODE=dev ts-node-dev -r dotenv/config --respawn --transpile-only src/index.ts",
|
"start:dev": "cross-env APP_MODE=dev PORT=3200 ts-node-dev -r dotenv/config --respawn --transpile-only src/index.ts",
|
||||||
|
"start:test": "cross-env APP_MODE=test PORT=3201 ts-node-dev -r dotenv/config --respawn --transpile-only src/index.ts",
|
||||||
"dev": "npm run start:dev",
|
"dev": "npm run start:dev",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"migrate:deploy": "npx prisma migrate deploy"
|
"migrate:deploy": "npx prisma migrate deploy"
|
||||||
|
|||||||
Binary file not shown.
@@ -16,6 +16,7 @@ import { TopBar } from './ui/TopBar';
|
|||||||
import { Modal } from './ui/Modal';
|
import { Modal } from './ui/Modal';
|
||||||
import { SideSheet } from './ui/SideSheet';
|
import { SideSheet } from './ui/SideSheet';
|
||||||
import { Checkbox } from './ui/Checkbox';
|
import { Checkbox } from './ui/Checkbox';
|
||||||
|
import { DatePicker } from './ui/DatePicker';
|
||||||
|
|
||||||
interface ProfileProps {
|
interface ProfileProps {
|
||||||
user: User;
|
user: User;
|
||||||
@@ -314,9 +315,13 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
|||||||
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><Ruler size={10} /> {t('height', lang)}</label>
|
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><Ruler size={10} /> {t('height', lang)}</label>
|
||||||
<input data-testid="profile-height-input" type="number" value={height} onChange={(e) => setHeight(e.target.value)} className="w-full bg-transparent text-on-surface focus:outline-none" />
|
<input data-testid="profile-height-input" type="number" value={height} onChange={(e) => setHeight(e.target.value)} className="w-full bg-transparent text-on-surface focus:outline-none" />
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-3 py-2">
|
<div>
|
||||||
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><Calendar size={10} /> {t('birth_date', lang)}</label>
|
<DatePicker
|
||||||
<input data-testid="profile-birth-date" type="date" value={birthDate} onChange={(e) => setBirthDate(e.target.value)} className="w-full bg-transparent text-on-surface focus:outline-none text-sm py-1" />
|
label={t('birth_date', lang)}
|
||||||
|
value={birthDate}
|
||||||
|
onChange={(val) => setBirthDate(val)}
|
||||||
|
testId="profile-birth-date"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-3 py-2">
|
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-3 py-2">
|
||||||
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><PersonStanding size={10} /> {t('gender', lang)}</label>
|
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><PersonStanding size={10} /> {t('gender', lang)}</label>
|
||||||
|
|||||||
281
src/components/ui/DatePicker.tsx
Normal file
281
src/components/ui/DatePicker.tsx
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
import React, { useState, useRef, useEffect, useId } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import { Calendar as CalendarIcon, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
|
import { Button } from './Button';
|
||||||
|
import { Ripple } from './Ripple';
|
||||||
|
|
||||||
|
interface DatePickerProps {
|
||||||
|
value: string; // YYYY-MM-DD
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
label: string;
|
||||||
|
placeholder?: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
disabled?: boolean;
|
||||||
|
testId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DatePicker: React.FC<DatePickerProps> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
label,
|
||||||
|
placeholder = 'Select date',
|
||||||
|
icon = <CalendarIcon size={16} />,
|
||||||
|
disabled = false,
|
||||||
|
testId
|
||||||
|
}) => {
|
||||||
|
const id = useId();
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [viewDate, setViewDate] = useState(value ? new Date(value) : new Date());
|
||||||
|
const [popoverPos, setPopoverPos] = useState({ top: 0, left: 0, width: 320 });
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const popoverRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Update popover position when opening or when window resizes
|
||||||
|
const updatePosition = () => {
|
||||||
|
if (containerRef.current) {
|
||||||
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
|
const scrollX = window.scrollX;
|
||||||
|
const scrollY = window.scrollY;
|
||||||
|
|
||||||
|
// Try to align with the right side of the input for better visibility
|
||||||
|
let left = rect.left + rect.width - 320 + scrollX;
|
||||||
|
// But don't go off-screen to the left
|
||||||
|
if (left < scrollX + 16) {
|
||||||
|
left = rect.left + scrollX;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPopoverPos({
|
||||||
|
top: rect.bottom + scrollY + 4,
|
||||||
|
left: left,
|
||||||
|
width: 320
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
updatePosition();
|
||||||
|
window.addEventListener('resize', updatePosition);
|
||||||
|
window.addEventListener('scroll', updatePosition, true);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', updatePosition);
|
||||||
|
window.removeEventListener('scroll', updatePosition, true);
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// Ensure viewDate is valid
|
||||||
|
useEffect(() => {
|
||||||
|
if (value) {
|
||||||
|
const date = new Date(value);
|
||||||
|
if (!isNaN(date.getTime())) {
|
||||||
|
setViewDate(date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (
|
||||||
|
containerRef.current && !containerRef.current.contains(event.target as Node) &&
|
||||||
|
popoverRef.current && !popoverRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
} else {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const handleInputClick = () => {
|
||||||
|
if (!disabled) {
|
||||||
|
const nextOpen = !isOpen;
|
||||||
|
if (nextOpen) {
|
||||||
|
updatePosition();
|
||||||
|
}
|
||||||
|
setIsOpen(nextOpen);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrevMonth = () => {
|
||||||
|
setViewDate(new Date(viewDate.getFullYear(), viewDate.getMonth() - 1, 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNextMonth = () => {
|
||||||
|
setViewDate(new Date(viewDate.getFullYear(), viewDate.getMonth() + 1, 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDateSelect = (day: number) => {
|
||||||
|
const selectedDate = new Date(viewDate.getFullYear(), viewDate.getMonth(), day);
|
||||||
|
const year = selectedDate.getFullYear();
|
||||||
|
const month = String(selectedDate.getMonth() + 1).padStart(2, '0');
|
||||||
|
const d = String(selectedDate.getDate()).padStart(2, '0');
|
||||||
|
onChange(`${year}-${month}-${d}`);
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const daysInMonth = (year: number, month: number) => new Date(year, month + 1, 0).getDate();
|
||||||
|
const firstDayOfMonth = (year: number, month: number) => new Date(year, month, 1).getDay();
|
||||||
|
|
||||||
|
const renderCalendar = () => {
|
||||||
|
const year = viewDate.getFullYear();
|
||||||
|
const month = viewDate.getMonth();
|
||||||
|
const daysCount = daysInMonth(year, month);
|
||||||
|
const startingDay = firstDayOfMonth(year, month);
|
||||||
|
const monthName = viewDate.toLocaleString('default', { month: 'long' });
|
||||||
|
|
||||||
|
const days = [];
|
||||||
|
for (let i = 0; i < startingDay; i++) {
|
||||||
|
days.push(<div key={`empty-${i}`} className="w-10 h-10" />);
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedDate = value ? new Date(value) : null;
|
||||||
|
const isSelected = (d: number) => {
|
||||||
|
return selectedDate &&
|
||||||
|
selectedDate.getFullYear() === year &&
|
||||||
|
selectedDate.getMonth() === month &&
|
||||||
|
selectedDate.getDate() === d;
|
||||||
|
};
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
const isToday = (d: number) => {
|
||||||
|
return today.getFullYear() === year &&
|
||||||
|
today.getMonth() === month &&
|
||||||
|
today.getDate() === d;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let d = 1; d <= daysCount; d++) {
|
||||||
|
days.push(
|
||||||
|
<button
|
||||||
|
key={d}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleDateSelect(d)}
|
||||||
|
className={`
|
||||||
|
relative w-10 h-10 rounded-full flex items-center justify-center text-sm transition-colors
|
||||||
|
${isSelected(d)
|
||||||
|
? 'bg-primary text-on-primary font-bold'
|
||||||
|
: isToday(d)
|
||||||
|
? 'text-primary border border-primary font-medium'
|
||||||
|
: 'text-on-surface hover:bg-surface-container-high'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<Ripple color={isSelected(d) ? 'rgba(255,255,255,0.3)' : 'rgba(var(--primary-rgb), 0.1)'} />
|
||||||
|
{d}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const calendarContent = (
|
||||||
|
<div
|
||||||
|
ref={popoverRef}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: popoverPos.top,
|
||||||
|
left: popoverPos.left,
|
||||||
|
zIndex: 9999,
|
||||||
|
}}
|
||||||
|
className="p-4 w-[320px] bg-surface-container-high rounded-2xl shadow-xl border border-outline-variant animate-in fade-in zoom-in duration-200 origin-top"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-medium text-on-surface">{monthName} {year}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Button variant="ghost" size="icon" onClick={handlePrevMonth} className="h-8 w-8">
|
||||||
|
<ChevronLeft size={18} />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" onClick={handleNextMonth} className="h-8 w-8">
|
||||||
|
<ChevronRight size={18} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-7 gap-1 mb-2">
|
||||||
|
{['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day, i) => (
|
||||||
|
<div key={i} className="text-center text-[10px] font-bold text-on-surface-variant h-8 flex items-center justify-center uppercase tracking-wider">
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-7 gap-1">
|
||||||
|
{days}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 pt-2 border-t border-outline-variant flex justify-end gap-2">
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => setIsOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return createPortal(calendarContent, document.body);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formattedValue = value ? new Date(value).toLocaleDateString() : '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full" ref={containerRef}>
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
relative group bg-surface-container-high rounded-t-[4px] border-b transition-all min-h-[56px] cursor-pointer
|
||||||
|
${isOpen ? 'border-primary border-b-2' : 'border-on-surface-variant hover:bg-on-surface/10'}
|
||||||
|
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
||||||
|
`}
|
||||||
|
onClick={handleInputClick}
|
||||||
|
>
|
||||||
|
<label className={`
|
||||||
|
absolute top-2 left-4 text-label-sm font-medium transition-colors flex items-center gap-1
|
||||||
|
${isOpen ? 'text-primary' : 'text-on-surface-variant'}
|
||||||
|
`}>
|
||||||
|
{icon} {label}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="w-full h-[56px] pt-5 pb-1 pl-4 pr-10 text-body-lg text-on-surface flex items-center">
|
||||||
|
{formattedValue || <span className="text-on-surface-variant/50">{placeholder}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute right-3 top-1/2 -translate-y-1/2 text-on-surface-variant">
|
||||||
|
<CalendarIcon size={20} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Hidden input for Playwright test compatibility */}
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
data-testid={testId}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
className="sr-only"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
width: '1px',
|
||||||
|
height: '1px',
|
||||||
|
padding: '0',
|
||||||
|
margin: '-1px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
clip: 'rect(0, 0, 0, 0)',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
borderWidth: '0',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
opacity: 0
|
||||||
|
}}
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isOpen && renderCalendar()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -6,7 +6,7 @@ test.describe('Smoke Tests - Backend Refactor', () => {
|
|||||||
const password = 'password123';
|
const password = 'password123';
|
||||||
|
|
||||||
// 1. Register
|
// 1. Register
|
||||||
const registerRes = await request.post('http://localhost:3001/api/auth/register', {
|
const registerRes = await request.post('http://localhost:3201/api/auth/register', {
|
||||||
data: { email, password }
|
data: { email, password }
|
||||||
});
|
});
|
||||||
expect(registerRes.ok()).toBeTruthy();
|
expect(registerRes.ok()).toBeTruthy();
|
||||||
@@ -17,7 +17,7 @@ test.describe('Smoke Tests - Backend Refactor', () => {
|
|||||||
const token = registerBody.data.token;
|
const token = registerBody.data.token;
|
||||||
|
|
||||||
// 2. Get Exercises
|
// 2. Get Exercises
|
||||||
const exercisesRes = await request.get('http://localhost:3001/api/exercises', {
|
const exercisesRes = await request.get('http://localhost:3201/api/exercises', {
|
||||||
headers: { Authorization: `Bearer ${token}` }
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
});
|
});
|
||||||
expect(exercisesRes.ok()).toBeTruthy();
|
expect(exercisesRes.ok()).toBeTruthy();
|
||||||
@@ -26,7 +26,7 @@ test.describe('Smoke Tests - Backend Refactor', () => {
|
|||||||
expect(Array.isArray(exercisesBody.data)).toBe(true);
|
expect(Array.isArray(exercisesBody.data)).toBe(true);
|
||||||
|
|
||||||
// 3. Create Session
|
// 3. Create Session
|
||||||
const sessionRes = await request.post('http://localhost:3001/api/sessions', {
|
const sessionRes = await request.post('http://localhost:3201/api/sessions', {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
data: {
|
data: {
|
||||||
id: "test-session-" + Date.now(),
|
id: "test-session-" + Date.now(),
|
||||||
@@ -40,7 +40,7 @@ test.describe('Smoke Tests - Backend Refactor', () => {
|
|||||||
expect(sessionBody.data).toHaveProperty('id');
|
expect(sessionBody.data).toHaveProperty('id');
|
||||||
|
|
||||||
// 4. Get Active Session
|
// 4. Get Active Session
|
||||||
const activeRes = await request.get('http://localhost:3001/api/sessions/active', {
|
const activeRes = await request.get('http://localhost:3201/api/sessions/active', {
|
||||||
headers: { Authorization: `Bearer ${token}` }
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
});
|
});
|
||||||
expect(activeRes.ok()).toBeTruthy();
|
expect(activeRes.ok()).toBeTruthy();
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ test.describe('V. User & System Management', () => {
|
|||||||
await page.getByRole('button', { name: 'Login' }).click();
|
await page.getByRole('button', { name: 'Login' }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.getByRole('button', { name: /Профиль|Profile/ }).click();
|
await page.getByRole('button', { name: /^(Профиль|Profile)$/ }).click();
|
||||||
await expect(page.getByRole('heading', { name: 'Профиль', exact: true })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Профиль', exact: true })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -498,7 +498,7 @@ test.describe('V. User & System Management', () => {
|
|||||||
const user = await createUniqueUser();
|
const user = await createUniqueUser();
|
||||||
|
|
||||||
const apiContext = await playwrightRequest.newContext({
|
const apiContext = await playwrightRequest.newContext({
|
||||||
baseURL: 'http://127.0.0.1:3001',
|
baseURL: 'http://127.0.0.1:3201',
|
||||||
extraHTTPHeaders: {
|
extraHTTPHeaders: {
|
||||||
'Authorization': `Bearer ${user.token}`
|
'Authorization': `Bearer ${user.token}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,8 @@ type MyFixtures = {
|
|||||||
// Extend the base test with our custom fixture
|
// Extend the base test with our custom fixture
|
||||||
export const test = base.extend<MyFixtures>({
|
export const test = base.extend<MyFixtures>({
|
||||||
createUniqueUser: async ({ }, use) => {
|
createUniqueUser: async ({ }, use) => {
|
||||||
// We use a new API context for setup to avoid polluting request history,
|
|
||||||
// although setup requests are usually separate anyway.
|
|
||||||
const apiContext = await request.newContext({
|
const apiContext = await request.newContext({
|
||||||
baseURL: 'http://127.0.0.1:3001' // Direct access to backend
|
baseURL: 'http://127.0.0.1:3201' // Direct access to backend
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup: Helper function to create a user
|
// Setup: Helper function to create a user
|
||||||
@@ -48,12 +46,6 @@ export const test = base.extend<MyFixtures>({
|
|||||||
|
|
||||||
// Use the fixture
|
// Use the fixture
|
||||||
await use(createUser);
|
await use(createUser);
|
||||||
|
|
||||||
// Cleanup: In a real "test:full" env with ephemeral db, cleanup might not be needed.
|
|
||||||
// But if we want to be clean, we can delete the user.
|
|
||||||
// Requires admin privileges usually, or specific delete-me endpoint.
|
|
||||||
// Given the requirements "delete own account" exists (5.6), we could theoretically login and delete.
|
|
||||||
// For now we skip auto-cleanup to keep it simple, assuming test DB is reset periodically.
|
|
||||||
},
|
},
|
||||||
|
|
||||||
createAdminUser: async ({ createUniqueUser }, use) => {
|
createAdminUser: async ({ createUniqueUser }, use) => {
|
||||||
@@ -65,7 +57,7 @@ export const test = base.extend<MyFixtures>({
|
|||||||
try {
|
try {
|
||||||
const { stdout, stderr } = await exec(`npx ts-node promote_admin.ts ${user.email}`, {
|
const { stdout, stderr } = await exec(`npx ts-node promote_admin.ts ${user.email}`, {
|
||||||
cwd: 'server',
|
cwd: 'server',
|
||||||
env: { ...process.env, APP_MODE: 'test', DATABASE_URL: 'file:d:/Coding/gymflow/server/test.db', DATABASE_URL_TEST: 'file:d:/Coding/gymflow/server/test.db' }
|
env: { ...process.env, APP_MODE: 'test', DATABASE_URL: 'file:d:/Coding/gymflow/server/prisma/test.db', DATABASE_URL_TEST: 'file:d:/Coding/gymflow/server/prisma/test.db' }
|
||||||
});
|
});
|
||||||
if (stderr) {
|
if (stderr) {
|
||||||
console.error(`Promote Admin Stderr: ${stderr}`);
|
console.error(`Promote Admin Stderr: ${stderr}`);
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ export default defineConfig(({ mode }) => {
|
|||||||
const env = loadEnv(mode, '.', '');
|
const env = loadEnv(mode, '.', '');
|
||||||
return {
|
return {
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 5173,
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:3001',
|
target: `http://localhost:${process.env.BACKEND_PORT || 3200}`,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user