Create Plan from Session. Top bar rounded
This commit is contained in:
Binary file not shown.
@@ -204,6 +204,22 @@ Comprehensive test plan for the GymFlow web application, covering authentication
|
|||||||
- The session starts with the selected plan's exercises.
|
- The session starts with the selected plan's exercises.
|
||||||
- The timer starts running.
|
- The timer starts running.
|
||||||
|
|
||||||
|
#### 2.5a. A. Workout Plans - Create Plan from Session
|
||||||
|
|
||||||
|
**File:** `tests/plan-from-session.spec.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. User completes a session with multiple sets (e.g., 2 sets of Pushups, 1 set of Squats).
|
||||||
|
2. Navigate to 'History'.
|
||||||
|
3. Click 'Create Plan' from the session menu.
|
||||||
|
4. Verify the Plan Editor opens.
|
||||||
|
5. **Verify Steps**: The plan should contain exactly 3 steps (Pushups, Pushups, Squats).
|
||||||
|
|
||||||
|
**Expected Results:**
|
||||||
|
- The Plan Editor is pre-filled.
|
||||||
|
- Plan steps mirror the session sets 1:1.
|
||||||
|
|
||||||
|
|
||||||
#### 2.6. B. Exercise Library - Create Custom Exercise (Strength)
|
#### 2.6. B. Exercise Library - Create Custom Exercise (Strength)
|
||||||
|
|
||||||
**File:** `tests/workout-management.spec.ts`
|
**File:** `tests/workout-management.spec.ts`
|
||||||
|
|||||||
@@ -64,7 +64,17 @@ Users can structure their training via Plans.
|
|||||||
* **Logic**: Supports reordering capabilities via drag-and-drop in UI.
|
* **Logic**: Supports reordering capabilities via drag-and-drop in UI.
|
||||||
* **3.2.2 Plan Deletion**
|
* **3.2.2 Plan Deletion**
|
||||||
* Standard soft or hard delete (Cascades to PlanExercises).
|
* Standard soft or hard delete (Cascades to PlanExercises).
|
||||||
* **3.2.3 AI Plan Creation**
|
* **3.2.3 Create Plan from Session**
|
||||||
|
* **Trigger**: Action menu in History session.
|
||||||
|
* **Logic**:
|
||||||
|
* Creates a new Plan pre-filled with data from the selected session.
|
||||||
|
* **Step Generation**: Mirrors sets 1:1. Every recorded set in the session becomes a distinct step in the plan (no collapsing).
|
||||||
|
* **Attributes**:
|
||||||
|
* `startWeight`: inherited from set.
|
||||||
|
* `restTime`: uses User's default rest timer setting.
|
||||||
|
* `isWeighted`: true if the specific set had weight > 0.
|
||||||
|
|
||||||
|
* **3.2.4 AI Plan Creation**
|
||||||
* **Trigger**: "Create with AI" option in Plans FAB Menu, or "Ask your AI coach" link from Tracker (when no plans exist).
|
* **Trigger**: "Create with AI" option in Plans FAB Menu, or "Ask your AI coach" link from Tracker (when no plans exist).
|
||||||
* **UI Flow**:
|
* **UI Flow**:
|
||||||
* Opens a dedicated Side Sheet in the Plans view.
|
* Opens a dedicated Side Sheet in the Plans view.
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Trash2, Calendar, Clock, ChevronDown, ChevronUp, History as HistoryIcon, Dumbbell, Ruler, Timer, Weight, Edit2, Gauge, Pencil, Save } from 'lucide-react';
|
import { createPortal } from 'react-dom';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Trash2, Calendar, Clock, ChevronDown, ChevronUp, History as HistoryIcon, Dumbbell, Ruler, Timer, Weight, Edit2, Gauge, Pencil, Save, MoreVertical, ClipboardList } from 'lucide-react';
|
||||||
import { TopBar } from './ui/TopBar';
|
import { TopBar } from './ui/TopBar';
|
||||||
import { WorkoutSession, ExerciseType, WorkoutSet, Language } from '../types';
|
import { WorkoutSession, ExerciseType, WorkoutSet, Language } from '../types';
|
||||||
import { t } from '../services/i18n';
|
import { t } from '../services/i18n';
|
||||||
@@ -9,6 +11,7 @@ import { useSession } from '../context/SessionContext';
|
|||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import { getExercises } from '../services/storage';
|
import { getExercises } from '../services/storage';
|
||||||
import { Button } from './ui/Button';
|
import { Button } from './ui/Button';
|
||||||
|
import { Ripple } from './ui/Ripple';
|
||||||
import { Card } from './ui/Card';
|
import { Card } from './ui/Card';
|
||||||
import { Modal } from './ui/Modal';
|
import { Modal } from './ui/Modal';
|
||||||
import { SideSheet } from './ui/SideSheet';
|
import { SideSheet } from './ui/SideSheet';
|
||||||
@@ -23,9 +26,11 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
|
|||||||
const { sessions, updateSession, deleteSession } = useSession();
|
const { sessions, updateSession, deleteSession } = useSession();
|
||||||
const { currentUser } = useAuth();
|
const { currentUser } = useAuth();
|
||||||
const userId = currentUser?.id || '';
|
const userId = currentUser?.id || '';
|
||||||
|
const navigate = useNavigate();
|
||||||
const [exercises, setExercises] = useState<import('../types').ExerciseDef[]>([]);
|
const [exercises, setExercises] = useState<import('../types').ExerciseDef[]>([]);
|
||||||
|
|
||||||
const [editingSession, setEditingSession] = useState<WorkoutSession | null>(null);
|
const [editingSession, setEditingSession] = useState<WorkoutSession | null>(null);
|
||||||
|
const [menuState, setMenuState] = useState<{ id: string, x: number, y: number } | null>(null);
|
||||||
|
|
||||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||||
const [deletingSetInfo, setDeletingSetInfo] = useState<{ sessionId: string, setId: string } | null>(null);
|
const [deletingSetInfo, setDeletingSetInfo] = useState<{ sessionId: string, setId: string } | null>(null);
|
||||||
@@ -221,28 +226,23 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-1">
|
<div className="relative">
|
||||||
<Button
|
<Button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setEditingSession(JSON.parse(JSON.stringify(session)));
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
|
setMenuState({
|
||||||
|
id: session.id,
|
||||||
|
x: rect.right + window.scrollX,
|
||||||
|
y: rect.bottom + window.scrollY
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
aria-label="Session Actions"
|
||||||
className="text-on-surface-variant hover:text-primary"
|
className="text-on-surface-variant hover:text-primary"
|
||||||
>
|
>
|
||||||
<Pencil size={24} />
|
<MoreVertical size={24} />
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setDeletingId(session.id);
|
|
||||||
}}
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="text-on-surface-variant hover:text-error"
|
|
||||||
>
|
|
||||||
<Trash2 size={24} />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -328,6 +328,70 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* MENU PORTAL */}
|
||||||
|
{menuState && typeof document !== 'undefined' && createPortal(
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setMenuState(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute bg-surface-container-high rounded-xl shadow-elevation-2 z-50 min-w-[160px] py-1 flex flex-col overflow-hidden animate-menu-enter origin-top-right"
|
||||||
|
style={{
|
||||||
|
top: menuState.y,
|
||||||
|
left: menuState.x,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const session = sessions.find(s => s.id === menuState.id);
|
||||||
|
if (session) {
|
||||||
|
navigate(`/plans?createFromSessionId=${session.id}`);
|
||||||
|
}
|
||||||
|
setMenuState(null);
|
||||||
|
}}
|
||||||
|
className="w-full relative overflow-hidden text-left px-4 py-3 hover:bg-on-surface/10 text-on-surface flex items-center gap-3 transition-colors text-sm font-medium"
|
||||||
|
>
|
||||||
|
<Ripple />
|
||||||
|
<ClipboardList size={18} />
|
||||||
|
{t('create_plan', lang) || 'Create Plan'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const session = sessions.find(s => s.id === menuState.id);
|
||||||
|
if (session) {
|
||||||
|
setEditingSession(JSON.parse(JSON.stringify(session)));
|
||||||
|
}
|
||||||
|
setMenuState(null);
|
||||||
|
}}
|
||||||
|
className="w-full relative overflow-hidden text-left px-4 py-3 hover:bg-on-surface/10 text-on-surface flex items-center gap-3 transition-colors text-sm font-medium"
|
||||||
|
>
|
||||||
|
<Ripple />
|
||||||
|
<Pencil size={18} />
|
||||||
|
{t('edit', lang)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setDeletingId(menuState.id);
|
||||||
|
setMenuState(null);
|
||||||
|
}}
|
||||||
|
className="w-full relative overflow-hidden text-left px-4 py-3 hover:bg-error-container/10 text-error flex items-center gap-3 transition-colors text-sm font-medium"
|
||||||
|
>
|
||||||
|
<Ripple color="rgba(242, 184, 181, 0.2)" />
|
||||||
|
<Trash2 size={18} />
|
||||||
|
{t('delete', lang)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
|
||||||
{/* DELETE CONFIRMATION MODAL */}
|
{/* DELETE CONFIRMATION MODAL */}
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={!!(deletingId || deletingSetInfo)}
|
isOpen={!!(deletingId || deletingSetInfo)}
|
||||||
|
|||||||
@@ -254,7 +254,44 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
|
|||||||
setShowAISheet(true);
|
setShowAISheet(true);
|
||||||
setSearchParams({});
|
setSearchParams({});
|
||||||
}
|
}
|
||||||
}, [searchParams, setSearchParams]);
|
|
||||||
|
const sourceSessionId = searchParams.get('createFromSessionId');
|
||||||
|
if (sourceSessionId && sessions.length > 0) {
|
||||||
|
const sourceSession = sessions.find(s => s.id === sourceSessionId);
|
||||||
|
if (sourceSession) {
|
||||||
|
handleCreateNew();
|
||||||
|
|
||||||
|
// Generate name
|
||||||
|
const dateStr = new Date(sourceSession.startTime).toLocaleDateString();
|
||||||
|
setName(sourceSession.planName || (lang === 'ru' ? `План от ${dateStr}` : `Plan from ${dateStr}`));
|
||||||
|
if (sourceSession.note) setDescription(sourceSession.note);
|
||||||
|
|
||||||
|
// Generate steps from sets
|
||||||
|
const newSteps: PlannedSet[] = [];
|
||||||
|
let lastExerciseId: string | null = null;
|
||||||
|
|
||||||
|
// Use default rest timer or 60s
|
||||||
|
const defaultRest = currentUser?.profile?.restTimerDefault || 60;
|
||||||
|
|
||||||
|
sourceSession.sets.forEach(set => {
|
||||||
|
// Mirror every set from the session to the plan
|
||||||
|
newSteps.push({
|
||||||
|
id: generateId(),
|
||||||
|
exerciseId: set.exerciseId,
|
||||||
|
exerciseName: set.exerciseName,
|
||||||
|
exerciseType: set.type,
|
||||||
|
isWeighted: (set.weight || 0) > 0,
|
||||||
|
restTimeSeconds: defaultRest
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setSteps(newSteps);
|
||||||
|
|
||||||
|
// Clear param so we don't re-run
|
||||||
|
setSearchParams({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [searchParams, setSearchParams, sessions, currentUser]);
|
||||||
|
|
||||||
const handleStart = (plan: WorkoutPlan) => {
|
const handleStart = (plan: WorkoutPlan) => {
|
||||||
if (plan.description && plan.description.trim().length > 0) {
|
if (plan.description && plan.description.trim().length > 0) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ interface TopBarProps {
|
|||||||
|
|
||||||
export const TopBar: React.FC<TopBarProps> = ({ title, icon: Icon, actions }) => {
|
export const TopBar: React.FC<TopBarProps> = ({ title, icon: Icon, actions }) => {
|
||||||
return (
|
return (
|
||||||
<div className="p-4 bg-surface-container shadow-elevation-1 flex items-center gap-3 z-10 shrink-0">
|
<div className="p-4 bg-surface-container shadow-elevation-1 flex items-center gap-3 z-10 shrink-0 rounded-b-[24px]">
|
||||||
{Icon && (
|
{Icon && (
|
||||||
<div className="w-10 h-10 rounded-full bg-secondary-container flex items-center justify-center">
|
<div className="w-10 h-10 rounded-full bg-secondary-container flex items-center justify-center">
|
||||||
<Icon size={20} className="text-on-secondary-container" />
|
<Icon size={20} className="text-on-secondary-container" />
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ const translations = {
|
|||||||
max: 'Max',
|
max: 'Max',
|
||||||
upto: 'Up to',
|
upto: 'Up to',
|
||||||
no_plan: 'No plan',
|
no_plan: 'No plan',
|
||||||
|
create_plan: 'Create Plan',
|
||||||
|
|
||||||
// Plans
|
// Plans
|
||||||
plans_empty: 'No plans created',
|
plans_empty: 'No plans created',
|
||||||
@@ -325,6 +326,7 @@ const translations = {
|
|||||||
max: 'Макс',
|
max: 'Макс',
|
||||||
upto: 'До',
|
upto: 'До',
|
||||||
no_plan: 'Без плана',
|
no_plan: 'Без плана',
|
||||||
|
create_plan: 'Создать план',
|
||||||
|
|
||||||
// Plans
|
// Plans
|
||||||
plans_empty: 'Нет созданных планов',
|
plans_empty: 'Нет созданных планов',
|
||||||
|
|||||||
@@ -63,15 +63,20 @@ export default {
|
|||||||
'elevation-4': '0px 2px 3px rgba(0, 0, 0, 0.3), 0px 6px 10px 4px rgba(0, 0, 0, 0.15)',
|
'elevation-4': '0px 2px 3px rgba(0, 0, 0, 0.3), 0px 6px 10px 4px rgba(0, 0, 0, 0.15)',
|
||||||
'elevation-5': '0px 4px 4px rgba(0, 0, 0, 0.3), 0px 8px 12px 6px rgba(0, 0, 0, 0.15)',
|
'elevation-5': '0px 4px 4px rgba(0, 0, 0, 0.3), 0px 8px 12px 6px rgba(0, 0, 0, 0.15)',
|
||||||
},
|
},
|
||||||
|
animation: {
|
||||||
|
ripple: 'ripple 600ms linear',
|
||||||
|
'menu-enter': 'menu-enter 200ms ease-out forwards',
|
||||||
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
ripple: {
|
ripple: {
|
||||||
'0%': { transform: 'scale(0)', opacity: '0.4' },
|
'0%': { transform: 'scale(0)', opacity: '0.4' },
|
||||||
'100%': { transform: 'scale(4)', opacity: '0' },
|
'100%': { transform: 'scale(4)', opacity: '0' },
|
||||||
|
},
|
||||||
|
'menu-enter': {
|
||||||
|
'0%': { opacity: '0', transform: 'scale(0.95) translateX(-100%)' }, // maintain the translate relative to position
|
||||||
|
'100%': { opacity: '1', transform: 'scale(1) translateX(-100%)' },
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
animation: {
|
|
||||||
ripple: 'ripple 600ms linear',
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
|||||||
158
tests/plan-from-session.spec.ts
Normal file
158
tests/plan-from-session.spec.ts
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import { test, expect } from './fixtures';
|
||||||
|
import { generateId } from '../src/utils/uuid';
|
||||||
|
import { ExerciseType } from '../src/types';
|
||||||
|
|
||||||
|
test('Create Plan from Session mirrors all sets 1:1', async ({ page, request, createUniqueUser }) => {
|
||||||
|
// 1. Setup User
|
||||||
|
const user = await createUniqueUser();
|
||||||
|
|
||||||
|
// 2. Create Exercises
|
||||||
|
const pushupsId = generateId();
|
||||||
|
const squatsId = generateId();
|
||||||
|
|
||||||
|
// Directly seed exercises via API (assuming a helper or just DB seed if possible,
|
||||||
|
// but here we might need to use the app or just mock the session data if we can inject it?
|
||||||
|
// Actually, createUniqueUser returns a token. We can use it to POST /exercises if that endpoint exists,
|
||||||
|
// or just rely on 'default' exercises if they are seeded.
|
||||||
|
// Let's use the 'saveSession' endpoint directly logic if we can, or just mock the DB state.
|
||||||
|
// Wait, the app uses local storage mostly or sync?
|
||||||
|
// Based on other tests (which I can't read right now but recall structure), they usually use UI or API helpers.
|
||||||
|
// I will assume I can just Login and then use UI or API.
|
||||||
|
// Let's use UI to just ensure clean state, or API `POST /sessions` if available.
|
||||||
|
// Based on `server/src/routes/sessions.ts` existing, I can POST session.
|
||||||
|
|
||||||
|
// Let's rely on standard UI flows or API.
|
||||||
|
// API is faster.
|
||||||
|
|
||||||
|
const token = user.token;
|
||||||
|
|
||||||
|
// Create Custom Exercises via API
|
||||||
|
await request.post('http://localhost:3000/api/exercises', {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
data: {
|
||||||
|
id: pushupsId,
|
||||||
|
name: 'Test Pushups',
|
||||||
|
type: 'BODYWEIGHT',
|
||||||
|
isUnilateral: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await request.post('http://localhost:3000/api/exercises', {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
data: {
|
||||||
|
id: squatsId,
|
||||||
|
name: 'Test Squats',
|
||||||
|
type: 'STRENGTH',
|
||||||
|
isUnilateral: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Create Session with 3 sets (A, A, B)
|
||||||
|
const sessionId = generateId();
|
||||||
|
const sessionData = {
|
||||||
|
id: sessionId,
|
||||||
|
startTime: Date.now() - 3600000, // 1 hour ago
|
||||||
|
endTime: Date.now(),
|
||||||
|
note: 'Killer workout',
|
||||||
|
type: 'STANDARD',
|
||||||
|
sets: [
|
||||||
|
{
|
||||||
|
id: generateId(),
|
||||||
|
exerciseId: pushupsId,
|
||||||
|
exerciseName: 'Test Pushups',
|
||||||
|
type: 'BODYWEIGHT',
|
||||||
|
reps: 10,
|
||||||
|
timestamp: Date.now() - 3000000,
|
||||||
|
completed: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: generateId(),
|
||||||
|
exerciseId: pushupsId,
|
||||||
|
exerciseName: 'Test Pushups',
|
||||||
|
type: 'BODYWEIGHT',
|
||||||
|
reps: 12,
|
||||||
|
weight: 10, // Weighted
|
||||||
|
timestamp: Date.now() - 2000000,
|
||||||
|
completed: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: generateId(),
|
||||||
|
exerciseId: squatsId,
|
||||||
|
exerciseName: 'Test Squats',
|
||||||
|
type: 'STRENGTH',
|
||||||
|
reps: 5,
|
||||||
|
weight: 100,
|
||||||
|
timestamp: Date.now() - 1000000,
|
||||||
|
completed: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
await request.post('http://localhost:3000/api/sessions', {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
data: sessionData
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Login and Navigate
|
||||||
|
await page.goto('http://localhost:3000/');
|
||||||
|
await page.fill('input[type="email"]', user.email);
|
||||||
|
await page.fill('input[type="password"]', user.password);
|
||||||
|
await page.click('button:has-text("Login")');
|
||||||
|
await page.waitForURL('**/tracker');
|
||||||
|
|
||||||
|
// 5. Go to History
|
||||||
|
await page.click('text=History');
|
||||||
|
|
||||||
|
// 6. Click Create Plan
|
||||||
|
const sessionCard = page.locator('div.bg-surface-container').first(); // Assuming it's the first card
|
||||||
|
await sessionCard.waitFor();
|
||||||
|
|
||||||
|
// Open Menu
|
||||||
|
await sessionCard.locator('button[aria-label="Session Actions"]').click();
|
||||||
|
// Click 'Create Plan'
|
||||||
|
await page.click('text=Create Plan');
|
||||||
|
|
||||||
|
// 7. Verify Redirection
|
||||||
|
await expect(page).toHaveURL(/.*plans\?createFromSessionId=.*/);
|
||||||
|
|
||||||
|
// 8. Verify Plan Editor Content
|
||||||
|
await expect(page.locator('h2')).toContainText('Plan Editor');
|
||||||
|
|
||||||
|
// Name should be "Plan from [Date]" or Session Name
|
||||||
|
// Note: Session had no planName, so it defaults to date.
|
||||||
|
// But we can check the Description matches 'Killer workout'
|
||||||
|
await expect(page.locator('textarea')).toHaveValue('Killer workout');
|
||||||
|
|
||||||
|
// 9. Verify 3 Steps (1:1 mapping)
|
||||||
|
// We expect 3 cards in the sortable list
|
||||||
|
const steps = page.locator('.dnd-sortable-item_content, div[class*="items-center"] > div.flex-1');
|
||||||
|
// Finding a robust selector for steps is tricky without specific test ids.
|
||||||
|
// The SortablePlanStep component has `div.text-base.font-medium.text-on-surface` for exercise name.
|
||||||
|
|
||||||
|
const stepNames = page.locator('div.text-base.font-medium.text-on-surface');
|
||||||
|
await expect(stepNames).toHaveCount(3);
|
||||||
|
|
||||||
|
await expect(stepNames.nth(0)).toHaveText('Test Pushups');
|
||||||
|
await expect(stepNames.nth(1)).toHaveText('Test Pushups');
|
||||||
|
await expect(stepNames.nth(2)).toHaveText('Test Squats');
|
||||||
|
|
||||||
|
// 10. Verify Weighted Flag Logic
|
||||||
|
// Set 1 (index 0): Unweighted
|
||||||
|
// Set 2 (index 1): Weighted (weight: 10)
|
||||||
|
// Set 3 (index 2): Weighted (weight: 100)
|
||||||
|
|
||||||
|
const checkboxes = page.locator('input[type="checkbox"]');
|
||||||
|
// Warning: there might be other checkboxes.
|
||||||
|
// SortablePlanStep has a checkbox for 'weighted'.
|
||||||
|
// Better to look for checked state within the step card.
|
||||||
|
|
||||||
|
// Step 1: Unchecked
|
||||||
|
await expect(page.locator('input[type="checkbox"]').nth(0)).not.toBeChecked();
|
||||||
|
|
||||||
|
// Step 2: Checked
|
||||||
|
await expect(page.locator('input[type="checkbox"]').nth(1)).toBeChecked();
|
||||||
|
|
||||||
|
// Step 3: Checked
|
||||||
|
await expect(page.locator('input[type="checkbox"]').nth(2)).toBeChecked();
|
||||||
|
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user