Side attribute editable for Unilateral exercises
This commit is contained in:
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -539,13 +539,16 @@ Comprehensive test plan for the GymFlow web application, covering authentication
|
|||||||
1. Start a Free Workout (or Plan with unilateral exercise).
|
1. Start a Free Workout (or Plan with unilateral exercise).
|
||||||
2. Select a Unilateral exercise (created in 2.12).
|
2. Select a Unilateral exercise (created in 2.12).
|
||||||
3. Enter Weight/Reps.
|
3. Enter Weight/Reps.
|
||||||
4. Select 'Left' from the Side selector.
|
4. Select 'L' from the Side selector.
|
||||||
5. Click 'Log Set'.
|
5. Click 'Log Set'.
|
||||||
6. Repeat for 'Right' side.
|
6. Repeat for 'R' side.
|
||||||
7. Repeat for 'Alternately' side.
|
7. Repeat for 'A' side.
|
||||||
|
8. Click 'Edit' on one of the logged sets.
|
||||||
|
9. Change side using the 'L'/'A'/'R' buttons and save.
|
||||||
|
|
||||||
**Expected Results:**
|
**Expected Results:**
|
||||||
- Sets are logged with the correct 'Left'/'Right'/'Alternately' indicators visible in the history.
|
- Sets are logged with the correct 'Left'/'Right'/'Alternately' indicators visible in the history.
|
||||||
|
- The Edit mode correctly shows 'L'/'A'/'R' buttons and updates the set side upon save.
|
||||||
|
|
||||||
#### 3.15. C. Active Session - Log Special Type Set
|
#### 3.15. C. Active Session - Log Special Type Set
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ Users can structure their training via Plans.
|
|||||||
* `PLYOMETRIC`: Requires **Reps**.
|
* `PLYOMETRIC`: Requires **Reps**.
|
||||||
* **3.3.2 Custom Exercises**
|
* **3.3.2 Custom Exercises**
|
||||||
* User can create new exercises.
|
* User can create new exercises.
|
||||||
* **Unilateral Flag**: Boolean flag `isUnilateral`. If true, sets recorded for this exercise can specify a `side` (LEFT/RIGHT/ALTERNATELY).
|
* **Unilateral Flag**: Boolean flag `isUnilateral`. If true, sets recorded for this exercise can specify a `side` (LEFT/Right/ALTERNATELY or L/R/A in UI).
|
||||||
|
* **Side Editing**: Users must be able to edit the side (checking L/A/R) in Active Session, History, and Quick Log modes.
|
||||||
* **Bodyweight %**: For bodyweight-based calculations.
|
* **Bodyweight %**: For bodyweight-based calculations.
|
||||||
|
|
||||||
### 3.4. Workout Tracking (The "Tracker")
|
### 3.4. Workout Tracking (The "Tracker")
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { WorkoutSession, ExerciseType, WorkoutSet, Language } from '../types';
|
|||||||
import { t } from '../services/i18n';
|
import { t } from '../services/i18n';
|
||||||
import { formatSetMetrics } from '../utils/setFormatting';
|
import { formatSetMetrics } from '../utils/setFormatting';
|
||||||
import { useSession } from '../context/SessionContext';
|
import { useSession } from '../context/SessionContext';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
import { getExercises } from '../services/storage';
|
||||||
import { Button } from './ui/Button';
|
import { Button } from './ui/Button';
|
||||||
import { Card } from './ui/Card';
|
import { Card } from './ui/Card';
|
||||||
import { Modal } from './ui/Modal';
|
import { Modal } from './ui/Modal';
|
||||||
@@ -15,11 +17,20 @@ interface HistoryProps {
|
|||||||
|
|
||||||
const History: React.FC<HistoryProps> = ({ lang }) => {
|
const History: React.FC<HistoryProps> = ({ lang }) => {
|
||||||
const { sessions, updateSession, deleteSession } = useSession();
|
const { sessions, updateSession, deleteSession } = useSession();
|
||||||
|
const { currentUser } = useAuth();
|
||||||
|
const userId = currentUser?.id || '';
|
||||||
|
const [exercises, setExercises] = useState<import('../types').ExerciseDef[]>([]);
|
||||||
|
|
||||||
const [editingSession, setEditingSession] = useState<WorkoutSession | null>(null);
|
const [editingSession, setEditingSession] = useState<WorkoutSession | 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);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!userId) return;
|
||||||
|
getExercises(userId).then(exs => setExercises(exs));
|
||||||
|
}, [userId]);
|
||||||
|
|
||||||
|
|
||||||
const calculateSessionWork = (session: WorkoutSession) => {
|
const calculateSessionWork = (session: WorkoutSession) => {
|
||||||
const bw = session.userBodyWeight || 70;
|
const bw = session.userBodyWeight || 70;
|
||||||
@@ -455,25 +466,36 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* Side Selector - Full width on mobile, 1 col on desktop if space */}
|
{/* Side Selector - Full width on mobile, 1 col on desktop if space */}
|
||||||
{set.side && (
|
{(() => {
|
||||||
<div className="bg-surface-container-high rounded px-2 py-1 col-span-2 sm:col-span-1 border border-outline-variant/30">
|
const exDef = exercises.find(e => e.id === set.exerciseId);
|
||||||
<label className="text-[10px] text-on-surface-variant font-bold block mb-1">{t('unilateral', lang)}</label>
|
const showSide = set.side || exDef?.isUnilateral;
|
||||||
<div className="flex bg-surface-container-low rounded p-0.5">
|
|
||||||
{(['LEFT', 'RIGHT', 'ALTERNATELY'] as const).map((sideOption) => (
|
if (!showSide) return null;
|
||||||
<button
|
|
||||||
key={sideOption}
|
return (
|
||||||
onClick={() => handleUpdateSet(set.id, 'side', sideOption)}
|
<div className="bg-surface-container-high rounded px-2 py-1 col-span-2 sm:col-span-1 border border-outline-variant/30">
|
||||||
className={`flex-1 text-[10px] py-1 rounded transition-colors ${set.side === sideOption
|
<label className="text-[10px] text-on-surface-variant font-bold block mb-1">{t('unilateral', lang)}</label>
|
||||||
? 'bg-primary/10 text-primary font-bold'
|
<div className="flex bg-surface-container-low rounded p-0.5">
|
||||||
: 'text-on-surface-variant hover:bg-surface-container'
|
{(['LEFT', 'ALTERNATELY', 'RIGHT'] as const).map((sideOption) => {
|
||||||
}`}
|
const labelMap: Record<string, string> = { LEFT: 'L', RIGHT: 'R', ALTERNATELY: 'A' };
|
||||||
>
|
return (
|
||||||
{t(sideOption.toLowerCase() as any, lang).slice(0, 3)}
|
<button
|
||||||
</button>
|
key={sideOption}
|
||||||
))}
|
onClick={() => handleUpdateSet(set.id, 'side', sideOption)}
|
||||||
|
title={t(sideOption.toLowerCase() as any, lang)}
|
||||||
|
className={`flex-1 text-[10px] py-1 rounded transition-colors ${set.side === sideOption
|
||||||
|
? 'bg-primary/10 text-primary font-bold'
|
||||||
|
: 'text-on-surface-variant hover:bg-surface-container'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{labelMap[sideOption]}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
)}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ const ActiveSessionView: React.FC<ActiveSessionViewProps> = ({ tracker, activeSe
|
|||||||
setEditDistance,
|
setEditDistance,
|
||||||
editHeight,
|
editHeight,
|
||||||
setEditHeight,
|
setEditHeight,
|
||||||
|
editSide,
|
||||||
|
setEditSide,
|
||||||
handleCancelEdit,
|
handleCancelEdit,
|
||||||
handleSaveEdit,
|
handleSaveEdit,
|
||||||
handleEditSet,
|
handleEditSet,
|
||||||
@@ -243,6 +245,39 @@ const ActiveSessionView: React.FC<ActiveSessionViewProps> = ({ tracker, activeSe
|
|||||||
placeholder="Height (cm)"
|
placeholder="Height (cm)"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{(() => {
|
||||||
|
const exDef = exercises.find(e => e.name === set.exerciseName); // Best effort matching by name since set might not have exerciseId deeply populated in some contexts, but id is safer.
|
||||||
|
// Actually set has exerciseId usually. Let's try to match by ID if possible, else name.
|
||||||
|
// But wait, ActiveSession sets might not have exerciseId if created ad-hoc? No, they should.
|
||||||
|
// Let's assume we can look up by name if id missing, or just check set.side presence.
|
||||||
|
// Detailed look: The session object has sets.
|
||||||
|
// Ideally check exDef.isUnilateral.
|
||||||
|
const isUnilateral = set.side || (exercises.find(e => e.name === set.exerciseName)?.isUnilateral);
|
||||||
|
|
||||||
|
if (isUnilateral) {
|
||||||
|
return (
|
||||||
|
<div className="col-span-2 flex bg-surface-container-high rounded p-0.5">
|
||||||
|
{(['LEFT', 'ALTERNATELY', 'RIGHT'] as const).map((side) => {
|
||||||
|
const labelMap: Record<string, string> = { LEFT: 'L', RIGHT: 'R', ALTERNATELY: 'A' };
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={side}
|
||||||
|
onClick={() => setEditSide(side)}
|
||||||
|
title={t(side.toLowerCase() as any, lang)}
|
||||||
|
className={`flex-1 text-[10px] py-1 rounded transition-colors ${editSide === side
|
||||||
|
? 'bg-primary/10 text-primary font-bold'
|
||||||
|
: 'text-on-surface-variant hover:bg-surface-container'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{labelMap[side]}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -101,24 +101,27 @@ const SetLogger: React.FC<SetLoggerProps> = ({ tracker, lang, onLogSet, isSporad
|
|||||||
<div className="flex items-center gap-2 bg-surface-container rounded-full p-1">
|
<div className="flex items-center gap-2 bg-surface-container rounded-full p-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => setUnilateralSide('LEFT')}
|
onClick={() => setUnilateralSide('LEFT')}
|
||||||
|
title={t('left', lang)}
|
||||||
className={`w-full text-center px-4 py-2 rounded-full text-sm font-medium transition-colors ${unilateralSide === 'LEFT' ? 'bg-primary-container text-on-primary-container' : 'text-on-surface-variant hover:bg-surface-container-high'
|
className={`w-full text-center px-4 py-2 rounded-full text-sm font-medium transition-colors ${unilateralSide === 'LEFT' ? 'bg-primary-container text-on-primary-container' : 'text-on-surface-variant hover:bg-surface-container-high'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{t('left', lang)}
|
L
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setUnilateralSide('ALTERNATELY')}
|
onClick={() => setUnilateralSide('ALTERNATELY')}
|
||||||
|
title={t('alternately', lang)}
|
||||||
className={`w-full text-center px-4 py-2 rounded-full text-sm font-medium transition-colors ${unilateralSide === 'ALTERNATELY' ? 'bg-tertiary-container text-on-tertiary-container' : 'text-on-surface-variant hover:bg-surface-container-high'
|
className={`w-full text-center px-4 py-2 rounded-full text-sm font-medium transition-colors ${unilateralSide === 'ALTERNATELY' ? 'bg-tertiary-container text-on-tertiary-container' : 'text-on-surface-variant hover:bg-surface-container-high'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{t('alternately', lang) || 'Alternately'}
|
A
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setUnilateralSide('RIGHT')}
|
onClick={() => setUnilateralSide('RIGHT')}
|
||||||
|
title={t('right', lang)}
|
||||||
className={`w-full text-center px-4 py-2 rounded-full text-sm font-medium transition-colors ${unilateralSide === 'RIGHT' ? 'bg-secondary-container text-on-secondary-container' : 'text-on-surface-variant hover:bg-surface-container-high'
|
className={`w-full text-center px-4 py-2 rounded-full text-sm font-medium transition-colors ${unilateralSide === 'RIGHT' ? 'bg-secondary-container text-on-secondary-container' : 'text-on-surface-variant hover:bg-surface-container-high'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{t('right', lang)}
|
R
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -143,6 +143,39 @@ const SporadicView: React.FC<SporadicViewProps> = ({ tracker, lang }) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{/* Side Selector */}
|
||||||
|
{(() => {
|
||||||
|
const exDef = exercises.find(e => e.name === editingSet.exerciseName);
|
||||||
|
const isUnilateral = editingSet.side || exDef?.isUnilateral;
|
||||||
|
|
||||||
|
if (isUnilateral) {
|
||||||
|
return (
|
||||||
|
<div className="bg-surface-container-high rounded-lg p-2">
|
||||||
|
<label className="text-sm text-on-surface-variant block mb-2">{t('unilateral', lang)}</label>
|
||||||
|
<div className="flex bg-surface-container rounded p-0.5">
|
||||||
|
{(['LEFT', 'ALTERNATELY', 'RIGHT'] as const).map((side) => {
|
||||||
|
const labelMap: Record<string, string> = { LEFT: 'L', RIGHT: 'R', ALTERNATELY: 'A' };
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={side}
|
||||||
|
onClick={() => setEditingSet({ ...editingSet, side })}
|
||||||
|
title={t(side.toLowerCase() as any, lang)}
|
||||||
|
className={`flex-1 text-xs py-2 rounded transition-colors ${editingSet.side === side
|
||||||
|
? 'bg-primary/10 text-primary font-bold'
|
||||||
|
: 'text-on-surface-variant hover:bg-surface-container-high'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{labelMap[side]}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})()}
|
||||||
|
|
||||||
{(editingSet.type === 'STRENGTH' || editingSet.type === 'BODYWEIGHT') && (
|
{(editingSet.type === 'STRENGTH' || editingSet.type === 'BODYWEIGHT') && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ export const useTracker = (props: any) => { // Props ignored/removed
|
|||||||
editDuration: form.editDuration, setEditDuration: form.setEditDuration,
|
editDuration: form.editDuration, setEditDuration: form.setEditDuration,
|
||||||
editDistance: form.editDistance, setEditDistance: form.setEditDistance,
|
editDistance: form.editDistance, setEditDistance: form.setEditDistance,
|
||||||
editHeight: form.editHeight, setEditHeight: form.setEditHeight,
|
editHeight: form.editHeight, setEditHeight: form.setEditHeight,
|
||||||
|
editSide: form.editSide, setEditSide: form.setEditSide,
|
||||||
|
|
||||||
isSporadicMode, setIsSporadicMode,
|
isSporadicMode, setIsSporadicMode,
|
||||||
sporadicSuccess,
|
sporadicSuccess,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const useWorkoutForm = ({ userId, onSetAdded, onUpdateSet }: UseWorkoutFo
|
|||||||
const [editDuration, setEditDuration] = useState<string>('');
|
const [editDuration, setEditDuration] = useState<string>('');
|
||||||
const [editDistance, setEditDistance] = useState<string>('');
|
const [editDistance, setEditDistance] = useState<string>('');
|
||||||
const [editHeight, setEditHeight] = useState<string>('');
|
const [editHeight, setEditHeight] = useState<string>('');
|
||||||
|
const [editSide, setEditSide] = useState<'LEFT' | 'RIGHT' | 'ALTERNATELY' | undefined>(undefined);
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
setWeight('');
|
setWeight('');
|
||||||
@@ -33,6 +34,7 @@ export const useWorkoutForm = ({ userId, onSetAdded, onUpdateSet }: UseWorkoutFo
|
|||||||
setDuration('');
|
setDuration('');
|
||||||
setDistance('');
|
setDistance('');
|
||||||
setHeight('');
|
setHeight('');
|
||||||
|
setEditSide(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateFormFromLastSet = async (exerciseId: string, exerciseType: ExerciseType, bodyWeightPercentage?: number) => {
|
const updateFormFromLastSet = async (exerciseId: string, exerciseType: ExerciseType, bodyWeightPercentage?: number) => {
|
||||||
@@ -104,6 +106,7 @@ export const useWorkoutForm = ({ userId, onSetAdded, onUpdateSet }: UseWorkoutFo
|
|||||||
setEditDuration(set.durationSeconds?.toString() || '');
|
setEditDuration(set.durationSeconds?.toString() || '');
|
||||||
setEditDistance(set.distanceMeters?.toString() || '');
|
setEditDistance(set.distanceMeters?.toString() || '');
|
||||||
setEditHeight(set.height?.toString() || '');
|
setEditHeight(set.height?.toString() || '');
|
||||||
|
setEditSide(set.side);
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveEdit = (set: WorkoutSet) => {
|
const saveEdit = (set: WorkoutSet) => {
|
||||||
@@ -113,7 +116,8 @@ export const useWorkoutForm = ({ userId, onSetAdded, onUpdateSet }: UseWorkoutFo
|
|||||||
...(editReps && { reps: parseInt(editReps) }),
|
...(editReps && { reps: parseInt(editReps) }),
|
||||||
...(editDuration && { durationSeconds: parseInt(editDuration) }),
|
...(editDuration && { durationSeconds: parseInt(editDuration) }),
|
||||||
...(editDistance && { distanceMeters: parseFloat(editDistance) }),
|
...(editDistance && { distanceMeters: parseFloat(editDistance) }),
|
||||||
...(editHeight && { height: parseFloat(editHeight) })
|
...(editHeight && { height: parseFloat(editHeight) }),
|
||||||
|
...(editSide && { side: editSide })
|
||||||
};
|
};
|
||||||
if (onUpdateSet) onUpdateSet(updatedSet);
|
if (onUpdateSet) onUpdateSet(updatedSet);
|
||||||
setEditingSetId(null);
|
setEditingSetId(null);
|
||||||
@@ -137,6 +141,7 @@ export const useWorkoutForm = ({ userId, onSetAdded, onUpdateSet }: UseWorkoutFo
|
|||||||
editDuration, setEditDuration,
|
editDuration, setEditDuration,
|
||||||
editDistance, setEditDistance,
|
editDistance, setEditDistance,
|
||||||
editHeight, setEditHeight,
|
editHeight, setEditHeight,
|
||||||
|
editSide, setEditSide,
|
||||||
resetForm,
|
resetForm,
|
||||||
updateFormFromLastSet,
|
updateFormFromLastSet,
|
||||||
prepareSetData,
|
prepareSetData,
|
||||||
|
|||||||
@@ -392,38 +392,73 @@ test.describe('III. Workout Tracking', () => {
|
|||||||
await page.getByRole('textbox', { name: /Select Exercise/i }).click();
|
await page.getByRole('textbox', { name: /Select Exercise/i }).click();
|
||||||
await page.getByText(exName).click();
|
await page.getByText(exName).click();
|
||||||
|
|
||||||
// Expect Left/Right selector
|
// Expect L/R/A selector
|
||||||
await expect(page.getByText(/Left/i)).toBeVisible();
|
await expect(page.getByRole('button', { name: 'L', exact: true })).toBeVisible();
|
||||||
|
await expect(page.getByRole('button', { name: 'R', exact: true })).toBeVisible();
|
||||||
|
await expect(page.getByRole('button', { name: 'A', exact: true })).toBeVisible();
|
||||||
|
|
||||||
// Log Left
|
// Helper to log a set
|
||||||
await page.getByText('Left').first().click();
|
const logSet = async (side: 'L' | 'R' | 'A') => {
|
||||||
await page.getByLabel('Weight (kg)').fill('20');
|
// Find the logger container (has 'Log Set' button)
|
||||||
await page.getByLabel('Reps').first().fill('10');
|
const logger = page.locator('div').filter({ has: page.getByRole('button', { name: /Log Set/i }) }).last();
|
||||||
await page.getByRole('button', { name: /Log Set/i }).click();
|
await expect(logger).toBeVisible();
|
||||||
|
|
||||||
// Verify Side and Metrics
|
// Select side
|
||||||
|
// Note: Side buttons are also inside the logger, but using global getByRole is okay if unique.
|
||||||
|
// Let's scope side as well for safety
|
||||||
|
await logger.getByRole('button', { name: side, exact: true }).click();
|
||||||
|
|
||||||
|
// Fill inputs scoped to logger
|
||||||
|
const weightInput = logger.getByLabel('Weight (kg)');
|
||||||
|
await weightInput.click();
|
||||||
|
await weightInput.fill('20');
|
||||||
|
|
||||||
|
// Reps - handle potential multiples if strict, but scoped should be unique
|
||||||
|
await logger.getByLabel('Reps').fill('10');
|
||||||
|
|
||||||
|
await logger.getByRole('button', { name: /Log Set/i }).click();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Log Left (L)
|
||||||
|
await logSet('L');
|
||||||
|
|
||||||
|
// Verify Side and Metrics in list (Left)
|
||||||
await expect(page.getByText('Left', { exact: true })).toBeVisible();
|
await expect(page.getByText('Left', { exact: true })).toBeVisible();
|
||||||
await expect(page.getByText('20 kg x 10 reps')).toBeVisible();
|
await expect(page.getByText(/20.*10/)).toBeVisible();
|
||||||
|
|
||||||
// Log Right
|
// Log Right (R)
|
||||||
await page.getByText('Right').first().click();
|
await logSet('R');
|
||||||
await page.getByLabel('Weight (kg)').fill('20');
|
|
||||||
await page.getByLabel('Reps').first().fill('10');
|
// Verify Right set
|
||||||
await page.getByRole('button', { name: /Log Set/i }).click();
|
|
||||||
await expect(page.getByText('Right', { exact: true })).toBeVisible();
|
await expect(page.getByText('Right', { exact: true })).toBeVisible();
|
||||||
|
// Use last() or filter to verify the new set's metrics if needed, but 'Right' presence confirms logging
|
||||||
|
// We'll proceed to editing
|
||||||
|
|
||||||
// Log Alternately
|
|
||||||
if (await page.getByText('Alternately').count() > 0) {
|
|
||||||
await page.getByText('Alternately').first().click();
|
|
||||||
} else {
|
|
||||||
// Fallback for i18n or exact text match if needed
|
|
||||||
await page.getByRole('button', { name: /Alternately|Alt/i }).click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.getByLabel('Weight (kg)').fill('20');
|
// Edit the Right set to be Alternately
|
||||||
await page.getByLabel('Reps').first().fill('10');
|
// Use a stable locator for the row (first item in history list)
|
||||||
await page.getByRole('button', { name: /Log Set/i }).click();
|
// The class 'bg-surface-container' and 'shadow-elevation-1' identifies the row card.
|
||||||
await expect(page.getByText(/Alternately|Alt/i).last()).toBeVisible();
|
// We use .first() because the list is reversed (newest first).
|
||||||
|
const rightSetRow = page.locator('.bg-surface-container.rounded-xl.shadow-elevation-1').first();
|
||||||
|
|
||||||
|
await rightSetRow.getByRole('button', { name: 'Edit' }).click();
|
||||||
|
|
||||||
|
// Verify we are in edit mode by finding the Save button
|
||||||
|
const saveButton = rightSetRow.getByRole('button', { name: /Save/i });
|
||||||
|
await expect(saveButton).toBeVisible();
|
||||||
|
|
||||||
|
// Change side to Alternately (A)
|
||||||
|
// Find 'A' button within the same row container which is now in edit mode
|
||||||
|
const aButton = rightSetRow.getByRole('button', { name: 'A', exact: true });
|
||||||
|
await expect(aButton).toBeVisible();
|
||||||
|
await aButton.click();
|
||||||
|
|
||||||
|
// Save
|
||||||
|
await saveButton.click();
|
||||||
|
|
||||||
|
// Verify update
|
||||||
|
// Use regex for Alternately to handle case/whitespace
|
||||||
|
await expect(page.getByText(/Alternately/i)).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('3.15 C. Active Session - Log Special Type Set', async ({ page, createUniqueUser, request }) => {
|
test('3.15 C. Active Session - Log Special Type Set', async ({ page, createUniqueUser, request }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user