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
Air Squats,BODYWEIGHT,1.0,false
Barbell Row,STRENGTH,0,false
Bench Press,STRENGTH,0,false
Bicep Curl,STRENGTH,0,true
Bulgarian Split-Squat Jumps,BODYWEIGHT,1.0,true
Bulgarian Split-Squats,BODYWEIGHT,1.0,true
Burpees,BODYWEIGHT,1.0,false
Calf Raise,STRENGTH,0,true
Chin-Ups,BODYWEIGHT,1.0,false
Cycling,CARDIO,0,false
Deadlift,STRENGTH,0,false
Dips,BODYWEIGHT,1.0,false
Dumbbell Curl,STRENGTH,0,true
Dumbbell Shoulder Press,STRENGTH,0,true
Face Pull,STRENGTH,0,false
Front Squat,STRENGTH,0,false
Hammer Curl,STRENGTH,0,true
Handstand,BODYWEIGHT,1.0,false
Hip Thrust,STRENGTH,0,false
Jump Rope,CARDIO,0,false
Lat Pulldown,STRENGTH,0,false
Leg Extension,STRENGTH,0,true
Leg Press,STRENGTH,0,false
Lunges,BODYWEIGHT,1.0,true
Mountain Climbers,CARDIO,0,false
Muscle-Up,BODYWEIGHT,1.0,false
Overhead Press,STRENGTH,0,false
Plank,STATIC,0,false
Pull-Ups,BODYWEIGHT,1.0,false
Push-Ups,BODYWEIGHT,0.65,false
Romanian Deadlift,STRENGTH,0,false
Rowing,CARDIO,0,false
Running,CARDIO,0,false
Russian Twist,BODYWEIGHT,0,false
Seated Cable Row,STRENGTH,0,false
Side Plank,STATIC,0,true
Sissy Squats,BODYWEIGHT,1.0,false
Sprint,CARDIO,0,false
Squat,STRENGTH,0,false
Treadmill,CARDIO,0,false
Tricep Extension,STRENGTH,0,false
Wall-Sit,STATIC,0,false
name,name_ru,type,bodyWeightPercentage,isUnilateral
Air Squats,Приседания,BODYWEIGHT,1.0,false
Barbell Row,Тяга штанги в наклоне,STRENGTH,0,false
Bench Press,Жим лежа,STRENGTH,0,false
Bicep Curl,Подъем на бицепс,STRENGTH,0,true
Bulgarian Split-Squat Jumps,Болгарские сплит-прыжки,BODYWEIGHT,1.0,true
Bulgarian Split-Squats,Болгарские сплит-приседания,BODYWEIGHT,1.0,true
Burpees,Берпи,BODYWEIGHT,1.0,false
Calf Raise,Подъем на носки,STRENGTH,0,true
Chin-Ups,Подтягивания обратным хватом,BODYWEIGHT,1.0,false
Cycling,Велосипед,CARDIO,0,false
Deadlift,Становая тяга,STRENGTH,0,false
Dips,Отжимания на брусьях,BODYWEIGHT,1.0,false
Dumbbell Curl,Сгибания рук с гантелями,STRENGTH,0,true
Dumbbell Shoulder Press,Жим гантелей сидя,STRENGTH,0,true
Face Pull,Тяга к лицу,STRENGTH,0,false
Front Squat,Фронтальный присед,STRENGTH,0,false
Hammer Curl,Сгибания "Молот",STRENGTH,0,true
Handstand,Стойка на руках,BODYWEIGHT,1.0,false
Hip Thrust,Ягодичный мостик,STRENGTH,0,false
Jump Rope,Скакалка,CARDIO,0,false
Lat Pulldown,Тяга верхнего блока,STRENGTH,0,false
Leg Extension,Разгибание ног в тренажере,STRENGTH,0,true
Leg Press,Жим ногами,STRENGTH,0,false
Lunges,Выпады,BODYWEIGHT,1.0,true
Mountain Climbers,Альпинист,CARDIO,0,false
Muscle-Up,Выход силой,BODYWEIGHT,1.0,false
Overhead Press,Армейский жим,STRENGTH,0,false
Plank,Планка,STATIC,0,false
Pull-Ups,Подтягивания,BODYWEIGHT,1.0,false
Push-Ups,Отжимания,BODYWEIGHT,0.65,false
Romanian Deadlift,Румынская тяга,STRENGTH,0,false
Rowing,Гребля,CARDIO,0,false
Running,Бег,CARDIO,0,false
Russian Twist,Русский твист,BODYWEIGHT,0,false
Seated Cable Row,Тяга блока к поясу,STRENGTH,0,false
Side Plank,Боковая планка,STATIC,0,true
Sissy Squats,Сисси-приседания,BODYWEIGHT,1.0,false
Sprint,Спринт,CARDIO,0,false
Squat,Приседания со штангой,STRENGTH,0,false
Treadmill,Беговая дорожка,CARDIO,0,false
Tricep Extension,Разгибание рук на трицепс,STRENGTH,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) {
try {
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}`);
}
// 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();
return;
}
@@ -77,12 +77,12 @@ async function ensureAdminUser() {
email: adminEmail,
password: hashed,
role: 'ADMIN',
profile: { create: { weight: 70 } },
profile: { create: { weight: 70, language: 'en' } },
},
});
// 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})`);
await prisma.$disconnect();

View File

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

View File

@@ -67,17 +67,13 @@ export class AuthService {
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 { password: _, ...userSafe } = user;
return { user: userSafe, token };
}
static async seedDefaultExercises(userId: string) {
static async seedDefaultExercises(userId: string, language: string = 'en') {
try {
// Ensure env is loaded from root (in case server didn't restart)
if (!process.env.DEFAULT_EXERCISES_CSV_PATH) {
@@ -110,6 +106,8 @@ export class AuthService {
const headers = lines[0].split(',').map(h => h.trim());
const exercisesToCreate = [];
const nameColumn = language === 'ru' ? 'name_ru' : 'name';
for (let i = 1; i < lines.length; i++) {
const cols = lines[i].split(',').map(c => c.trim());
if (cols.length < headers.length) continue;
@@ -117,10 +115,12 @@ export class AuthService {
const row: any = {};
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({
userId,
name: row.name,
name: exerciseName,
type: row.type,
bodyWeightPercentage: row.bodyWeightPercentage ? parseFloat(row.bodyWeightPercentage) : 0,
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) {
const hashed = await bcrypt.hash(newPassword, 10);
await prisma.user.update({
where: { id: userId },
data: {
password: hashed,
isFirstLogin: false
password: hashed
}
});
}

View File

@@ -8,6 +8,7 @@ import AICoach from './components/AICoach';
import Plans from './components/Plans';
import Login from './components/Login';
import Profile from './components/Profile';
import InitializeAccount from './components/InitializeAccount';
import { Language, User } from './types';
import { getSystemLanguage } from './services/i18n';
import { useAuth } from './context/AuthContext';
@@ -49,6 +50,10 @@ function App() {
return <Navigate to="/login" />;
}
if (currentUser?.isFirstLogin && location.pathname !== '/initialize') {
return <Navigate to="/initialize" />;
}
return (
<div className="h-[100dvh] w-screen bg-surface text-on-surface font-sans flex flex-col md:flex-row overflow-hidden">
{currentUser && (
@@ -66,6 +71,17 @@ function App() {
<Navigate to="/" />
)
} />
<Route path="/initialize" element={
currentUser && currentUser.isFirstLogin ? (
<InitializeAccount
onInitialized={updateUser}
language={language}
onLanguageChange={setLanguage}
/>
) : (
<Navigate to="/" />
)
} />
<Route path="/" element={
<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) {
const res = await changePassword(tempUser.id, newPassword);
if (res.success) {
const updatedUser = { ...tempUser, isFirstLogin: false };
const updatedUser = { ...tempUser };
onLogin(updatedUser);
} else {
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' };
}
};
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',
have_account: 'Already have an account? Login',
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
date: 'Date',
@@ -253,6 +259,12 @@ const translations = {
register_btn: 'Зарегистрироваться',
have_account: 'Уже есть аккаунт? Войти',
need_account: 'Нет аккаунта? Регистрация',
init_title: 'Настройка аккаунта',
init_desc: 'Добро пожаловать! Выберите предпочтительный язык.',
init_select_lang: 'Выберите язык',
init_start: 'Начать работу',
init_lang_en_desc: 'Интерфейс и названия упражнений по умолчанию будут на английском',
init_lang_ru_desc: 'Интерфейс и названия упражнений по умолчанию будут на русском',
// General
date: 'Дата',

View File

@@ -12,65 +12,61 @@ test.describe('I. Core & Authentication', () => {
// Helper to handle first login if needed
async function handleFirstLogin(page: any) {
// Wait for either Free Workout (already logged in/not first time)
// OR Change Password heading
// OR Error message
try {
const heading = page.getByRole('heading', { name: /Change Password/i });
const dashboard = page.getByText('Free Workout');
const loginButton = page.getByRole('button', { name: 'Login' });
console.log('Starting handleFirstLogin helper...');
const dashboard = page.getByText(/Free Workout|Свободная тренировка/i).first();
const changePass = page.getByRole('heading', { name: /Change Password|Смена пароля/i });
const initAcc = page.getByRole('heading', { name: /Setup Your Account|Настройка аккаунта/i });
// Race condition: wait for one of these to appear
// We use a small polling or just wait logic.
// Playwright doesn't have "race" for locators easily without Promise.race
// 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.');
for (let i = 0; i < 30; i++) {
if (await dashboard.isVisible()) {
console.log('Dashboard visible.');
return;
}
// Check for login error
const error = page.locator('.text-error');
if (await error.isVisible()) {
console.log('Login Error detected:', await error.textContent());
throw new Error(`Login failed: ${await error.textContent()}`);
if (await changePass.isVisible()) {
console.log('Change Password screen detected. Handling...');
await page.getByLabel(/New Password|Новый пароль/i).fill('StrongNewPass123!');
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
test('1.1 Login - Successful Authentication', async ({ page, createUniqueUser }) => {
const user = await createUniqueUser();
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password);
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel(/Email/i).fill(user.email);
await page.getByLabel(/Password|Пароль/i).fill(user.password);
await page.getByRole('button', { name: /Login|Войти/i }).click();
await handleFirstLogin(page);
// Expect redirection to dashboard
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
test('1.2 Login - Invalid Credentials', async ({ page }) => {
await page.getByLabel('Email').fill('invalid@user.com');
await page.getByLabel('Password').fill('wrongpassword');
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel(/Email/i).fill('invalid@user.com');
await page.getByLabel(/Password|Пароль/i).fill('wrongpassword');
await page.getByRole('button', { name: /Login|Войти/i }).click();
await expect(page.getByText('Invalid credentials')).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 }) => {
const user = await createUniqueUser();
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password);
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel(/Email/i).fill(user.email);
await page.getByLabel(/Password|Пароль/i).fill(user.password);
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
await page.getByLabel('New Password').fill('123');
await page.getByRole('button', { name: /Save|Change/i }).click();
await expect(page.getByText('Password too short')).toBeVisible();
await page.getByLabel(/New Password|Новый пароль/i).fill('123');
await page.getByRole('button', { name: /Save|Change|Сохранить/i }).click();
await expect(page.getByText(/Password too short|Пароль слишком короткий/i)).toBeVisible();
// 1.3 Test successful change
await page.getByLabel('New Password').fill('StrongNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
await page.getByLabel(/New Password|Новый пароль/i).fill('StrongNewPass123!');
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
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 }) => {
await page.getByRole('combobox').selectOption('en');
await expect(page.getByLabel('Email')).toBeVisible();
await expect(page.getByRole('button', { name: 'Login' })).toBeVisible();
await expect(page.getByLabel(/Email/i)).toBeVisible();
await expect(page.getByRole('button', { name: /Login|Войти/i })).toBeVisible();
});
// 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 }) => {
const user = await createUniqueUser();
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password);
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel(/Email/i).fill(user.email);
await page.getByLabel(/Password|Пароль/i).fill(user.password);
await page.getByRole('button', { name: /Login|Войти/i }).click();
await handleFirstLogin(page);
// Set viewport to desktop
await page.setViewportSize({ width: 1280, height: 720 });
await expect(page.getByRole('button', { name: 'Tracker' }).first()).toBeVisible();
await expect(page.getByRole('button', { name: 'Plans' }).first()).toBeVisible();
await expect(page.getByRole('button', { name: /Tracker|Трекер/i }).first()).toBeVisible();
await expect(page.getByRole('button', { name: /Plans|Планы/i }).first()).toBeVisible();
});
// 1.8. B. Navigation - Mobile Bottom Navigation Bar
test('1.8 Navigation - Mobile Bottom Navigation Bar', async ({ page, createUniqueUser }) => {
const user = await createUniqueUser();
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password);
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel(/Email/i).fill(user.email);
await page.getByLabel(/Password|Пароль/i).fill(user.password);
await page.getByRole('button', { name: /Login|Войти/i }).click();
await handleFirstLogin(page);
@@ -145,7 +144,44 @@ test.describe('I. Core & Authentication', () => {
await page.waitForTimeout(500); // Allow layout transition
// 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();
});
});