Default exercises are created in selected language. Initial GUI added

This commit is contained in:
AG
2025-12-18 22:16:39 +02:00
parent 78d4a10f33
commit abffb52af1
12 changed files with 332 additions and 117 deletions

View File

@@ -1,43 +1,43 @@
name,type,bodyWeightPercentage,isUnilateral name,name_ru,type,bodyWeightPercentage,isUnilateral
Air Squats,BODYWEIGHT,1.0,false Air Squats,Приседания,BODYWEIGHT,1.0,false
Barbell Row,STRENGTH,0,false Barbell Row,Тяга штанги в наклоне,STRENGTH,0,false
Bench Press,STRENGTH,0,false Bench Press,Жим лежа,STRENGTH,0,false
Bicep Curl,STRENGTH,0,true Bicep Curl,Подъем на бицепс,STRENGTH,0,true
Bulgarian Split-Squat Jumps,BODYWEIGHT,1.0,true Bulgarian Split-Squat Jumps,Болгарские сплит-прыжки,BODYWEIGHT,1.0,true
Bulgarian Split-Squats,BODYWEIGHT,1.0,true Bulgarian Split-Squats,Болгарские сплит-приседания,BODYWEIGHT,1.0,true
Burpees,BODYWEIGHT,1.0,false Burpees,Берпи,BODYWEIGHT,1.0,false
Calf Raise,STRENGTH,0,true Calf Raise,Подъем на носки,STRENGTH,0,true
Chin-Ups,BODYWEIGHT,1.0,false Chin-Ups,Подтягивания обратным хватом,BODYWEIGHT,1.0,false
Cycling,CARDIO,0,false Cycling,Велосипед,CARDIO,0,false
Deadlift,STRENGTH,0,false Deadlift,Становая тяга,STRENGTH,0,false
Dips,BODYWEIGHT,1.0,false Dips,Отжимания на брусьях,BODYWEIGHT,1.0,false
Dumbbell Curl,STRENGTH,0,true Dumbbell Curl,Сгибания рук с гантелями,STRENGTH,0,true
Dumbbell Shoulder Press,STRENGTH,0,true Dumbbell Shoulder Press,Жим гантелей сидя,STRENGTH,0,true
Face Pull,STRENGTH,0,false Face Pull,Тяга к лицу,STRENGTH,0,false
Front Squat,STRENGTH,0,false Front Squat,Фронтальный присед,STRENGTH,0,false
Hammer Curl,STRENGTH,0,true Hammer Curl,Сгибания "Молот",STRENGTH,0,true
Handstand,BODYWEIGHT,1.0,false Handstand,Стойка на руках,BODYWEIGHT,1.0,false
Hip Thrust,STRENGTH,0,false Hip Thrust,Ягодичный мостик,STRENGTH,0,false
Jump Rope,CARDIO,0,false Jump Rope,Скакалка,CARDIO,0,false
Lat Pulldown,STRENGTH,0,false Lat Pulldown,Тяга верхнего блока,STRENGTH,0,false
Leg Extension,STRENGTH,0,true Leg Extension,Разгибание ног в тренажере,STRENGTH,0,true
Leg Press,STRENGTH,0,false Leg Press,Жим ногами,STRENGTH,0,false
Lunges,BODYWEIGHT,1.0,true Lunges,Выпады,BODYWEIGHT,1.0,true
Mountain Climbers,CARDIO,0,false Mountain Climbers,Альпинист,CARDIO,0,false
Muscle-Up,BODYWEIGHT,1.0,false Muscle-Up,Выход силой,BODYWEIGHT,1.0,false
Overhead Press,STRENGTH,0,false Overhead Press,Армейский жим,STRENGTH,0,false
Plank,STATIC,0,false Plank,Планка,STATIC,0,false
Pull-Ups,BODYWEIGHT,1.0,false Pull-Ups,Подтягивания,BODYWEIGHT,1.0,false
Push-Ups,BODYWEIGHT,0.65,false Push-Ups,Отжимания,BODYWEIGHT,0.65,false
Romanian Deadlift,STRENGTH,0,false Romanian Deadlift,Румынская тяга,STRENGTH,0,false
Rowing,CARDIO,0,false Rowing,Гребля,CARDIO,0,false
Running,CARDIO,0,false Running,Бег,CARDIO,0,false
Russian Twist,BODYWEIGHT,0,false Russian Twist,Русский твист,BODYWEIGHT,0,false
Seated Cable Row,STRENGTH,0,false Seated Cable Row,Тяга блока к поясу,STRENGTH,0,false
Side Plank,STATIC,0,true Side Plank,Боковая планка,STATIC,0,true
Sissy Squats,BODYWEIGHT,1.0,false Sissy Squats,Сисси-приседания,BODYWEIGHT,1.0,false
Sprint,CARDIO,0,false Sprint,Спринт,CARDIO,0,false
Squat,STRENGTH,0,false Squat,Приседания со штангой,STRENGTH,0,false
Treadmill,CARDIO,0,false Treadmill,Беговая дорожка,CARDIO,0,false
Tricep Extension,STRENGTH,0,false Tricep Extension,Разгибание рук на трицепс,STRENGTH,0,false
Wall-Sit,STATIC,0,false Wall-Sit,Стульчик у стены,STATIC,0,false
Can't render this file because it contains an unexpected character in line 18 and column 30.

Binary file not shown.

View File

@@ -83,6 +83,21 @@ export class AuthController {
} }
} }
static async initializeAccount(req: any, res: Response) {
try {
const userId = req.user.userId;
const { language } = req.body;
if (!language) {
return sendError(res, 'Language is required', 400);
}
const user = await AuthService.initializeUser(userId, language);
return sendSuccess(res, { user });
} catch (error: any) {
logger.error('Error in initializeAccount', { error });
return sendError(res, error.message || 'Server error', 500);
}
}
static async getAllUsers(req: any, res: Response) { static async getAllUsers(req: any, res: Response) {
try { try {
if (req.user.role !== 'ADMIN') { if (req.user.role !== 'ADMIN') {

View File

@@ -65,7 +65,7 @@ async function ensureAdminUser() {
console.info(` Admin user exists but with different email: ${existingAdmin.email}. Expected: ${adminEmail}`); console.info(` Admin user exists but with different email: ${existingAdmin.email}. Expected: ${adminEmail}`);
} }
// Even if admin exists, ensure exercises are seeded (will skip if already has them) // Even if admin exists, ensure exercises are seeded (will skip if already has them)
await AuthService.seedDefaultExercises(existingAdmin.id); await AuthService.seedDefaultExercises(existingAdmin.id, 'en');
await prisma.$disconnect(); await prisma.$disconnect();
return; return;
} }
@@ -77,12 +77,12 @@ async function ensureAdminUser() {
email: adminEmail, email: adminEmail,
password: hashed, password: hashed,
role: 'ADMIN', role: 'ADMIN',
profile: { create: { weight: 70 } }, profile: { create: { weight: 70, language: 'en' } },
}, },
}); });
// Seed exercises for new admin // Seed exercises for new admin
await AuthService.seedDefaultExercises(admin.id); await AuthService.seedDefaultExercises(admin.id, 'en');
console.info(`✅ Admin user created and exercises seeded (email: ${adminEmail})`); console.info(`✅ Admin user created and exercises seeded (email: ${adminEmail})`);
await prisma.$disconnect(); await prisma.$disconnect();

View File

@@ -16,6 +16,7 @@ router.use(authenticateToken); // Standard middleware now
router.get('/me', AuthController.getCurrentUser); router.get('/me', AuthController.getCurrentUser);
router.post('/change-password', validate(changePasswordSchema), AuthController.changePassword); router.post('/change-password', validate(changePasswordSchema), AuthController.changePassword);
router.patch('/profile', validate(updateProfileSchema), AuthController.updateProfile); router.patch('/profile', validate(updateProfileSchema), AuthController.updateProfile);
router.post('/initialize', AuthController.initializeAccount);
// Admin routes // Admin routes
router.get('/users', AuthController.getAllUsers); router.get('/users', AuthController.getAllUsers);

View File

@@ -67,17 +67,13 @@ export class AuthService {
include: { profile: true } include: { profile: true }
}); });
// Seed default exercises
// Seed default exercises
await this.seedDefaultExercises(user.id);
const token = jwt.sign({ userId: user.id, role: user.role }, JWT_SECRET); const token = jwt.sign({ userId: user.id, role: user.role }, JWT_SECRET);
const { password: _, ...userSafe } = user; const { password: _, ...userSafe } = user;
return { user: userSafe, token }; return { user: userSafe, token };
} }
static async seedDefaultExercises(userId: string) { static async seedDefaultExercises(userId: string, language: string = 'en') {
try { try {
// Ensure env is loaded from root (in case server didn't restart) // Ensure env is loaded from root (in case server didn't restart)
if (!process.env.DEFAULT_EXERCISES_CSV_PATH) { if (!process.env.DEFAULT_EXERCISES_CSV_PATH) {
@@ -110,6 +106,8 @@ export class AuthService {
const headers = lines[0].split(',').map(h => h.trim()); const headers = lines[0].split(',').map(h => h.trim());
const exercisesToCreate = []; const exercisesToCreate = [];
const nameColumn = language === 'ru' ? 'name_ru' : 'name';
for (let i = 1; i < lines.length; i++) { for (let i = 1; i < lines.length; i++) {
const cols = lines[i].split(',').map(c => c.trim()); const cols = lines[i].split(',').map(c => c.trim());
if (cols.length < headers.length) continue; if (cols.length < headers.length) continue;
@@ -117,10 +115,12 @@ export class AuthService {
const row: any = {}; const row: any = {};
headers.forEach((h, idx) => row[h] = cols[idx]); headers.forEach((h, idx) => row[h] = cols[idx]);
if (row.name && row.type) { const exerciseName = row[nameColumn] || row['name'];
if (exerciseName && row.type) {
exercisesToCreate.push({ exercisesToCreate.push({
userId, userId,
name: row.name, name: exerciseName,
type: row.type, type: row.type,
bodyWeightPercentage: row.bodyWeightPercentage ? parseFloat(row.bodyWeightPercentage) : 0, bodyWeightPercentage: row.bodyWeightPercentage ? parseFloat(row.bodyWeightPercentage) : 0,
isUnilateral: row.isUnilateral === 'true', isUnilateral: row.isUnilateral === 'true',
@@ -153,14 +153,34 @@ export class AuthService {
} }
} }
static async initializeUser(userId: string, language: string) {
// Update profile language
await prisma.userProfile.upsert({
where: { userId },
update: { language },
create: { userId, language, weight: 70 }
});
// Seed exercises in that language
await this.seedDefaultExercises(userId, language);
// Mark as first login done
await prisma.user.update({
where: { id: userId },
data: { isFirstLogin: false }
});
// Return updated user
return this.getUser(userId);
}
static async changePassword(userId: string, newPassword: string) { static async changePassword(userId: string, newPassword: string) {
const hashed = await bcrypt.hash(newPassword, 10); const hashed = await bcrypt.hash(newPassword, 10);
await prisma.user.update({ await prisma.user.update({
where: { id: userId }, where: { id: userId },
data: { data: {
password: hashed, password: hashed
isFirstLogin: false
} }
}); });
} }

View File

@@ -8,6 +8,7 @@ import AICoach from './components/AICoach';
import Plans from './components/Plans'; import Plans from './components/Plans';
import Login from './components/Login'; import Login from './components/Login';
import Profile from './components/Profile'; import Profile from './components/Profile';
import InitializeAccount from './components/InitializeAccount';
import { Language, User } from './types'; import { Language, User } from './types';
import { getSystemLanguage } from './services/i18n'; import { getSystemLanguage } from './services/i18n';
import { useAuth } from './context/AuthContext'; import { useAuth } from './context/AuthContext';
@@ -49,6 +50,10 @@ function App() {
return <Navigate to="/login" />; return <Navigate to="/login" />;
} }
if (currentUser?.isFirstLogin && location.pathname !== '/initialize') {
return <Navigate to="/initialize" />;
}
return ( return (
<div className="h-[100dvh] w-screen bg-surface text-on-surface font-sans flex flex-col md:flex-row overflow-hidden"> <div className="h-[100dvh] w-screen bg-surface text-on-surface font-sans flex flex-col md:flex-row overflow-hidden">
{currentUser && ( {currentUser && (
@@ -66,6 +71,17 @@ function App() {
<Navigate to="/" /> <Navigate to="/" />
) )
} /> } />
<Route path="/initialize" element={
currentUser && currentUser.isFirstLogin ? (
<InitializeAccount
onInitialized={updateUser}
language={language}
onLanguageChange={setLanguage}
/>
) : (
<Navigate to="/" />
)
} />
<Route path="/" element={ <Route path="/" element={
<Tracker lang={language} /> <Tracker lang={language} />
} /> } />

View File

@@ -0,0 +1,99 @@
import React, { useState } from 'react';
import { initializeAccount } from '../services/auth';
import { User, Language } from '../types';
import { Globe, ArrowRight, Check } from 'lucide-react';
import { t } from '../services/i18n';
interface InitializeAccountProps {
onInitialized: (user: User) => void;
language: Language;
onLanguageChange: (lang: Language) => void;
}
const InitializeAccount: React.FC<InitializeAccountProps> = ({ onInitialized, language, onLanguageChange }) => {
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState('');
const handleInitialize = async () => {
setIsSubmitting(true);
setError('');
const res = await initializeAccount(language);
if (res.success && res.user) {
onInitialized(res.user);
} else {
setError(res.error || 'Failed to initialize account');
setIsSubmitting(false);
}
};
const languages: { code: Language; label: string; desc: string }[] = [
{ code: 'en', label: 'English', desc: t('init_lang_en_desc', language) },
{ code: 'ru', label: 'Русский', desc: t('init_lang_ru_desc', language) },
];
return (
<div className="h-screen bg-surface flex flex-col items-center justify-center p-6 bg-surface">
<div className="w-full max-w-sm bg-surface-container p-8 rounded-[28px] shadow-elevation-2 flex flex-col items-center">
<div className="w-16 h-16 bg-primary-container rounded-2xl flex items-center justify-center text-on-primary-container mb-6 shadow-elevation-1">
<Globe size={32} />
</div>
<h1 className="text-2xl font-normal text-on-surface mb-2 text-center">
{t('init_title', language)}
</h1>
<p className="text-sm text-on-surface-variant mb-8 text-center balance">
{t('init_desc', language)}
</p>
{error && (
<div className="w-full text-error text-sm text-center bg-error-container/10 p-3 rounded-xl mb-6 border border-error/10">
{error}
</div>
)}
<div className="w-full space-y-3 mb-8">
{languages.map((lang) => (
<button
key={lang.code}
onClick={() => onLanguageChange(lang.code)}
className={`w-full p-4 rounded-2xl border-2 transition-all flex items-center justify-between group ${language === lang.code
? 'border-primary bg-primary/5'
: 'border-outline-variant/30 hover:border-outline-variant hover:bg-surface-container-high'
}`}
>
<div className="text-left">
<div className={`font-medium ${language === lang.code ? 'text-primary' : 'text-on-surface'}`}>
{lang.label}
</div>
<div className="text-xs text-on-surface-variant">
{lang.desc}
</div>
</div>
{language === lang.code && (
<div className="w-6 h-6 bg-primary text-on-primary rounded-full flex items-center justify-center">
<Check size={14} strokeWidth={3} />
</div>
)}
</button>
))}
</div>
<button
onClick={handleInitialize}
disabled={isSubmitting}
className="w-full py-4 bg-primary text-on-primary rounded-full font-medium text-lg shadow-elevation-1 flex items-center justify-center gap-2 hover:shadow-elevation-2 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? (
<div className="w-6 h-6 border-2 border-on-primary/30 border-t-on-primary rounded-full animate-spin" />
) : (
<>
{t('init_start', language)} <ArrowRight size={20} />
</>
)}
</button>
</div>
</div>
);
};
export default InitializeAccount;

View File

@@ -41,7 +41,7 @@ const Login: React.FC<LoginProps> = ({ onLogin, language, onLanguageChange }) =>
if (tempUser && newPassword.length >= 4) { if (tempUser && newPassword.length >= 4) {
const res = await changePassword(tempUser.id, newPassword); const res = await changePassword(tempUser.id, newPassword);
if (res.success) { if (res.success) {
const updatedUser = { ...tempUser, isFirstLogin: false }; const updatedUser = { ...tempUser };
onLogin(updatedUser); onLogin(updatedUser);
} else { } else {
setError(res.error || t('change_pass_error', language)); setError(res.error || t('change_pass_error', language));

View File

@@ -149,3 +149,19 @@ export const getMe = async (): Promise<{ success: boolean; user?: User; error?:
return { success: false, error: 'Failed to fetch user' }; return { success: false, error: 'Failed to fetch user' };
} }
}; };
export const initializeAccount = async (language: string): Promise<{ success: boolean; user?: User; error?: string }> => {
try {
const res = await api.post<ApiResponse<{ user: User }>>('/auth/initialize', { language });
if (res.success && res.data) {
return { success: true, user: res.data.user };
}
return { success: false, error: res.error };
} catch (e: any) {
try {
const err = JSON.parse(e.message);
return { success: false, error: err.error || 'Failed to initialize account' };
} catch {
return { success: false, error: 'Failed to initialize account' };
}
}
};

View File

@@ -36,6 +36,12 @@ const translations = {
register_btn: 'Register', register_btn: 'Register',
have_account: 'Already have an account? Login', have_account: 'Already have an account? Login',
need_account: 'Need an account? Register', need_account: 'Need an account? Register',
init_title: 'Setup Your Account',
init_desc: 'Welcome! Choose your preferred language.',
init_select_lang: 'Select Language',
init_start: 'Get Started',
init_lang_en_desc: 'GUI and default exercise names will be in English',
init_lang_ru_desc: 'GUI and default exercise names will be in Russian',
// General // General
date: 'Date', date: 'Date',
@@ -253,6 +259,12 @@ const translations = {
register_btn: 'Зарегистрироваться', register_btn: 'Зарегистрироваться',
have_account: 'Уже есть аккаунт? Войти', have_account: 'Уже есть аккаунт? Войти',
need_account: 'Нет аккаунта? Регистрация', need_account: 'Нет аккаунта? Регистрация',
init_title: 'Настройка аккаунта',
init_desc: 'Добро пожаловать! Выберите предпочтительный язык.',
init_select_lang: 'Выберите язык',
init_start: 'Начать работу',
init_lang_en_desc: 'Интерфейс и названия упражнений по умолчанию будут на английском',
init_lang_ru_desc: 'Интерфейс и названия упражнений по умолчанию будут на русском',
// General // General
date: 'Дата', date: 'Дата',

View File

@@ -12,65 +12,61 @@ test.describe('I. Core & Authentication', () => {
// Helper to handle first login if needed // Helper to handle first login if needed
async function handleFirstLogin(page: any) { async function handleFirstLogin(page: any) {
// Wait for either Free Workout (already logged in/not first time) console.log('Starting handleFirstLogin helper...');
// OR Change Password heading const dashboard = page.getByText(/Free Workout|Свободная тренировка/i).first();
// OR Error message const changePass = page.getByRole('heading', { name: /Change Password|Смена пароля/i });
try { const initAcc = page.getByRole('heading', { name: /Setup Your Account|Настройка аккаунта/i });
const heading = page.getByRole('heading', { name: /Change Password/i });
const dashboard = page.getByText('Free Workout');
const loginButton = page.getByRole('button', { name: 'Login' });
// Race condition: wait for one of these to appear for (let i = 0; i < 30; i++) {
// We use a small polling or just wait logic. if (await dashboard.isVisible()) {
// Playwright doesn't have "race" for locators easily without Promise.race console.log('Dashboard visible.');
// Simple approach: Check if Change Password appears within 5s
await expect(heading).toBeVisible({ timeout: 5000 });
// If we are here, Change Password is visible
console.log('Change Password screen detected. Handling...');
await page.getByLabel('New Password').fill('StrongNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
// Now expect dashboard
await expect(dashboard).toBeVisible();
console.log('Password changed. Dashboard visible.');
} catch (e) {
// If Change Password didn't appear, maybe we are already at dashboard?
if (await page.getByText('Free Workout').isVisible()) {
console.log('Already at Dashboard.');
return; return;
} }
// Check for login error
const error = page.locator('.text-error'); if (await changePass.isVisible()) {
if (await error.isVisible()) { console.log('Change Password screen detected. Handling...');
console.log('Login Error detected:', await error.textContent()); await page.getByLabel(/New Password|Новый пароль/i).fill('StrongNewPass123!');
throw new Error(`Login failed: ${await error.textContent()}`); await page.getByRole('button', { name: /Save|Change|Сохранить/i }).click();
// Wait a bit for transition
await page.waitForTimeout(1000);
continue;
} }
// Note: If none of the above, it might be a clean login that just worked fast or failed silently
if (await initAcc.isVisible()) {
console.log('Initialization screen detected. Handling...');
await page.getByRole('button', { name: /Get Started|Начать работу/i }).click();
// Wait a bit for transition
await page.waitForTimeout(1000);
continue;
}
await page.waitForTimeout(500);
} }
// Final check with assertion to fail the test if not reached
await expect(dashboard).toBeVisible({ timeout: 5000 });
} }
// 1.1. A. Login - Successful Authentication // 1.1. A. Login - Successful Authentication
test('1.1 Login - Successful Authentication', async ({ page, createUniqueUser }) => { test('1.1 Login - Successful Authentication', async ({ page, createUniqueUser }) => {
const user = await createUniqueUser(); const user = await createUniqueUser();
await page.getByLabel('Email').fill(user.email); await page.getByLabel(/Email/i).fill(user.email);
await page.getByLabel('Password').fill(user.password); await page.getByLabel(/Password|Пароль/i).fill(user.password);
await page.getByRole('button', { name: 'Login' }).click(); await page.getByRole('button', { name: /Login|Войти/i }).click();
await handleFirstLogin(page); await handleFirstLogin(page);
// Expect redirection to dashboard // Expect redirection to dashboard
await expect(page).not.toHaveURL(/\/login/); await expect(page).not.toHaveURL(/\/login/);
await expect(page.getByText('Free Workout')).toBeVisible(); await expect(page.getByText(/Free Workout|Свободная тренировка/i).first()).toBeVisible();
}); });
// 1.2. A. Login - Invalid Credentials // 1.2. A. Login - Invalid Credentials
test('1.2 Login - Invalid Credentials', async ({ page }) => { test('1.2 Login - Invalid Credentials', async ({ page }) => {
await page.getByLabel('Email').fill('invalid@user.com'); await page.getByLabel(/Email/i).fill('invalid@user.com');
await page.getByLabel('Password').fill('wrongpassword'); await page.getByLabel(/Password|Пароль/i).fill('wrongpassword');
await page.getByRole('button', { name: 'Login' }).click(); await page.getByRole('button', { name: /Login|Войти/i }).click();
await expect(page.getByText('Invalid credentials')).toBeVisible(); await expect(page.getByText('Invalid credentials')).toBeVisible();
await expect(page.getByRole('button', { name: 'Login' })).toBeVisible(); await expect(page.getByRole('button', { name: 'Login' })).toBeVisible();
@@ -79,31 +75,34 @@ test.describe('I. Core & Authentication', () => {
test('1.3 & 1.4 Login - First-Time Password Change', async ({ page, createUniqueUser }) => { test('1.3 & 1.4 Login - First-Time Password Change', async ({ page, createUniqueUser }) => {
const user = await createUniqueUser(); const user = await createUniqueUser();
await page.getByLabel('Email').fill(user.email); await page.getByLabel(/Email/i).fill(user.email);
await page.getByLabel('Password').fill(user.password); await page.getByLabel(/Password|Пароль/i).fill(user.password);
await page.getByRole('button', { name: 'Login' }).click(); await page.getByRole('button', { name: /Login|Войти/i }).click();
await expect(page.getByRole('heading', { name: /Change Password/i }).first()).toBeVisible({ timeout: 10000 }); await expect(page.getByRole('heading', { name: /Change Password|Смена пароля/i }).first()).toBeVisible({ timeout: 10000 });
// 1.4 Test short password // 1.4 Test short password
await page.getByLabel('New Password').fill('123'); await page.getByLabel(/New Password|Новый пароль/i).fill('123');
await page.getByRole('button', { name: /Save|Change/i }).click(); await page.getByRole('button', { name: /Save|Change|Сохранить/i }).click();
await expect(page.getByText('Password too short')).toBeVisible(); await expect(page.getByText(/Password too short|Пароль слишком короткий/i)).toBeVisible();
// 1.3 Test successful change // 1.3 Test successful change
await page.getByLabel('New Password').fill('StrongNewPass123!'); await page.getByLabel(/New Password|Новый пароль/i).fill('StrongNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click(); await page.getByRole('button', { name: /Save|Change|Сохранить/i }).click();
// Now we should be on Setup Account page
await expect(page.getByRole('heading', { name: /Setup Your Account|Настройка аккаунта/i })).toBeVisible();
await page.getByRole('button', { name: /Get Started|Начать работу/i }).click();
// Now we should be logged in // Now we should be logged in
await expect(page.getByText('Free Workout')).toBeVisible(); await expect(page.getByText(/Free Workout|Свободная тренировка/i).first()).toBeVisible();
}); });
// 1.5. A. Login - Language Selection (English)
test('1.5 Login - Language Selection (English)', async ({ page }) => { test('1.5 Login - Language Selection (English)', async ({ page }) => {
await page.getByRole('combobox').selectOption('en'); await page.getByRole('combobox').selectOption('en');
await expect(page.getByLabel('Email')).toBeVisible(); await expect(page.getByLabel(/Email/i)).toBeVisible();
await expect(page.getByRole('button', { name: 'Login' })).toBeVisible(); await expect(page.getByRole('button', { name: /Login|Войти/i })).toBeVisible();
}); });
// 1.6. A. Login - Language Selection (Russian) // 1.6. A. Login - Language Selection (Russian)
@@ -116,26 +115,26 @@ test.describe('I. Core & Authentication', () => {
test('1.7 Navigation - Desktop Navigation Rail', async ({ page, createUniqueUser }) => { test('1.7 Navigation - Desktop Navigation Rail', async ({ page, createUniqueUser }) => {
const user = await createUniqueUser(); const user = await createUniqueUser();
await page.getByLabel('Email').fill(user.email); await page.getByLabel(/Email/i).fill(user.email);
await page.getByLabel('Password').fill(user.password); await page.getByLabel(/Password|Пароль/i).fill(user.password);
await page.getByRole('button', { name: 'Login' }).click(); await page.getByRole('button', { name: /Login|Войти/i }).click();
await handleFirstLogin(page); await handleFirstLogin(page);
// Set viewport to desktop // Set viewport to desktop
await page.setViewportSize({ width: 1280, height: 720 }); await page.setViewportSize({ width: 1280, height: 720 });
await expect(page.getByRole('button', { name: 'Tracker' }).first()).toBeVisible(); await expect(page.getByRole('button', { name: /Tracker|Трекер/i }).first()).toBeVisible();
await expect(page.getByRole('button', { name: 'Plans' }).first()).toBeVisible(); await expect(page.getByRole('button', { name: /Plans|Планы/i }).first()).toBeVisible();
}); });
// 1.8. B. Navigation - Mobile Bottom Navigation Bar // 1.8. B. Navigation - Mobile Bottom Navigation Bar
test('1.8 Navigation - Mobile Bottom Navigation Bar', async ({ page, createUniqueUser }) => { test('1.8 Navigation - Mobile Bottom Navigation Bar', async ({ page, createUniqueUser }) => {
const user = await createUniqueUser(); const user = await createUniqueUser();
await page.getByLabel('Email').fill(user.email); await page.getByLabel(/Email/i).fill(user.email);
await page.getByLabel('Password').fill(user.password); await page.getByLabel(/Password|Пароль/i).fill(user.password);
await page.getByRole('button', { name: 'Login' }).click(); await page.getByRole('button', { name: /Login|Войти/i }).click();
await handleFirstLogin(page); await handleFirstLogin(page);
@@ -145,7 +144,44 @@ test.describe('I. Core & Authentication', () => {
await page.waitForTimeout(500); // Allow layout transition await page.waitForTimeout(500); // Allow layout transition
// Verify visibility of mobile nav items // Verify visibility of mobile nav items
await expect(page.getByRole('button', { name: 'Tracker' }).last()).toBeVisible(); await expect(page.getByRole('button', { name: /Tracker|Трекер/i }).last()).toBeVisible();
});
// 1.9. C. Initialization - Russian Language Seeding
test('1.9 Initialization - Russian Language Seeding', async ({ page, createUniqueUser }) => {
const user = await createUniqueUser();
await page.getByLabel(/Email/i).fill(user.email);
await page.getByLabel(/Password|Пароль/i).fill(user.password);
await page.getByRole('button', { name: /Login|Войти/i }).click();
// Handle password change
await expect(page.getByRole('heading', { name: /Change Password|Смена пароля/i })).toBeVisible();
await page.getByLabel(/New Password|Новый пароль/i).fill('StrongNewPass123!');
await page.getByRole('button', { name: /Save|Change|Сохранить/i }).click();
// Handle initialization - Select Russian
await expect(page.getByRole('heading', { name: /Setup Your Account|Настройка аккаунта/i })).toBeVisible();
await page.getByText('Русский').click();
await page.getByRole('button', { name: /Начать работу|Get Started/i }).click();
// Expect dashboard
await expect(page.getByText('Свободная тренировка')).toBeVisible();
// Verify some exercise is in Russian
await page.getByText(/Свободная тренировка|Free Workout/i).first().click();
await page.getByLabel(/Выберите упражнение|Select Exercise/i).click();
// "Air Squats" should be "Приседания" in suggestions
await expect(page.getByRole('button', { name: 'Приседания', exact: true })).toBeVisible();
await page.getByRole('button', { name: 'Приседания', exact: true }).click();
// Verify it's selected in the input
const exerciseInput = page.getByLabel(/Выберите упражнение|Select Exercise/i);
await expect(exerciseInput).toHaveValue('Приседания');
// Verify "Log Set" button is now in Russian
await expect(page.getByRole('button', { name: /Записать подход|Log Set/i })).toBeVisible();
}); });
}); });