AI Coach messages bookmarking. Top bar refined.
This commit is contained in:
92
tests/ai-coach.spec.ts
Normal file
92
tests/ai-coach.spec.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
|
||||
import { test, expect } from './fixtures';
|
||||
|
||||
test.describe('VII. AI Coach Features', () => {
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
// Helper to handle first login if needed (copied from core-auth)
|
||||
async function handleFirstLogin(page: any) {
|
||||
try {
|
||||
const heading = page.getByRole('heading', { name: /Change Password/i });
|
||||
const dashboard = page.getByText('Free Workout');
|
||||
|
||||
await expect(heading).toBeVisible({ timeout: 5000 });
|
||||
await page.getByLabel('New Password').fill('StrongNewPass123!');
|
||||
await page.getByRole('button', { name: /Save|Change/i }).click();
|
||||
await expect(dashboard).toBeVisible();
|
||||
} catch (e) {
|
||||
if (await page.getByText('Free Workout').isVisible()) return;
|
||||
}
|
||||
}
|
||||
|
||||
test('7.1 AI Coach - Basic Conversation & Markdown', async ({ page, createUniqueUser }) => {
|
||||
const user = await createUniqueUser();
|
||||
// Login
|
||||
await page.getByLabel('Email').fill(user.email);
|
||||
await page.getByLabel('Password').fill(user.password);
|
||||
await page.getByRole('button', { name: 'Login' }).click();
|
||||
await handleFirstLogin(page);
|
||||
|
||||
// Navigate to AI Coach
|
||||
await page.getByText('AI Coach').click();
|
||||
|
||||
// Type message
|
||||
const input = page.getByPlaceholder(/Ask your AI coach/i);
|
||||
await input.fill('How to do a pushup?');
|
||||
await page.getByRole('button', { name: /Send/i }).click();
|
||||
|
||||
// Verify response (Mocked or Real - expecting Real from previous context)
|
||||
// Since we can't easily mock backend without more setup, we wait for *any* response
|
||||
await expect(page.locator('.prose')).toBeVisible({ timeout: 15000 });
|
||||
|
||||
// Check for markdown rendering (e.g., strong tags or list items if AI returns them)
|
||||
// This is a bit flaky with real AI, but checking for visibility is a good start.
|
||||
});
|
||||
|
||||
test('7.2, 7.3, 7.4 AI Coach - Bookmark Flow', async ({ page, createUniqueUser }) => {
|
||||
const user = await createUniqueUser();
|
||||
// Login
|
||||
await page.getByLabel('Email').fill(user.email);
|
||||
await page.getByLabel('Password').fill(user.password);
|
||||
await page.getByRole('button', { name: 'Login' }).click();
|
||||
await handleFirstLogin(page);
|
||||
|
||||
await page.getByText('AI Coach').click();
|
||||
|
||||
// Send message
|
||||
await page.getByPlaceholder(/Ask your AI coach/i).fill('Tell me a short fitness tip');
|
||||
await page.getByRole('button', { name: /Send/i }).click();
|
||||
|
||||
// Wait for response bubble
|
||||
const responseBubble = page.locator('.prose').first();
|
||||
await expect(responseBubble).toBeVisible({ timeout: 15000 });
|
||||
|
||||
// 7.2 Bookmark
|
||||
// Find bookmark button within the message container.
|
||||
// Assuming the layout puts actions near the message.
|
||||
// We look for the Bookmark icon button.
|
||||
const bookmarkBtn = page.getByRole('button', { name: /Bookmark/i }).first();
|
||||
await bookmarkBtn.click();
|
||||
|
||||
// Expect success snackbar
|
||||
await expect(page.getByText(/Message saved/i)).toBeVisible();
|
||||
|
||||
// 7.3 View Saved
|
||||
await page.getByRole('button', { name: /Saved/i }).click(); // The TopBar action
|
||||
await expect(page.getByText('Saved Messages')).toBeVisible(); // Sheet title
|
||||
|
||||
// Verify content is there
|
||||
await expect(page.getByText(/fitness tip/i)).toBeVisible(); // Part of our prompt/response context usually
|
||||
|
||||
// 7.4 Delete Bookmark
|
||||
const deleteBtn = page.getByRole('button', { name: /Delete/i }).first();
|
||||
await deleteBtn.click();
|
||||
|
||||
// Verify removal
|
||||
await expect(deleteBtn).not.toBeVisible();
|
||||
});
|
||||
|
||||
});
|
||||
136
tests/repro_edit_fields.spec.ts
Normal file
136
tests/repro_edit_fields.spec.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { test, expect } from './fixtures';
|
||||
|
||||
test.describe('Reproduction - Edit Modal Fields', () => {
|
||||
|
||||
test('Verify Edit Fields for different Exercise Types', async ({ page, createUniqueUser, request }) => {
|
||||
const user = await createUniqueUser();
|
||||
// Login
|
||||
await page.goto('/');
|
||||
await page.getByLabel('Email').fill(user.email);
|
||||
await page.getByLabel('Password').fill(user.password);
|
||||
await page.getByRole('button', { name: 'Login' }).click();
|
||||
|
||||
// Wait for dashboard or password change
|
||||
try {
|
||||
const heading = page.getByRole('heading', { name: /Change Password/i });
|
||||
const dashboard = page.getByText('Free Workout');
|
||||
await expect(heading.or(dashboard)).toBeVisible({ timeout: 10000 });
|
||||
if (await heading.isVisible()) {
|
||||
await page.getByLabel('New Password').fill('StrongNewPass123!');
|
||||
await page.getByRole('button', { name: /Save|Change/i }).click();
|
||||
await expect(dashboard).toBeVisible();
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Login flow exception (might be benign if already logged in):', e);
|
||||
}
|
||||
|
||||
// Seed exercises of different types
|
||||
const types = [
|
||||
{ type: 'PLYOMETRIC', name: 'Plyo Test', expectedFields: ['Reps'] },
|
||||
{ type: 'STRENGTH', name: 'Strength Test', expectedFields: ['Weight', 'Reps'] },
|
||||
{ type: 'CARDIO', name: 'Cardio Test', expectedFields: ['Time', 'Distance'] },
|
||||
{ type: 'STATIC', name: 'Static Test', expectedFields: ['Time', 'Weight', 'Body Weight'] }, // Check if Weight is expected based on History.tsx analysis
|
||||
{ type: 'BODYWEIGHT', name: 'Bodyweight Test', expectedFields: ['Reps', 'Body Weight', 'Weight'] },
|
||||
{ type: 'HIGH_JUMP', name: 'High Jump Test', expectedFields: ['Height'] },
|
||||
{ type: 'LONG_JUMP', name: 'Long Jump Test', expectedFields: ['Distance'] },
|
||||
];
|
||||
|
||||
const exIds: Record<string, string> = {};
|
||||
|
||||
for (const t of types) {
|
||||
const resp = await request.post('/api/exercises', {
|
||||
data: { name: t.name, type: t.type },
|
||||
headers: { 'Authorization': `Bearer ${user.token}` }
|
||||
});
|
||||
expect(resp.ok()).toBeTruthy();
|
||||
const created = await resp.json();
|
||||
// Adjust if the response structure is different (e.g. created.exercise)
|
||||
exIds[t.name] = created.id || created.exercise?.id || created.data?.id;
|
||||
}
|
||||
|
||||
await page.reload();
|
||||
|
||||
// Construct a session payload
|
||||
const now = Date.now();
|
||||
const setsStub = types.map(t => {
|
||||
const set: any = {
|
||||
exerciseId: exIds[t.name],
|
||||
timestamp: now + 1000,
|
||||
completed: true
|
||||
};
|
||||
if (t.type === 'STRENGTH' || t.type === 'BODYWEIGHT' || t.type === 'PLYOMETRIC') set.reps = 10;
|
||||
if (t.type === 'STRENGTH' || t.type === 'BODYWEIGHT' || t.type === 'STATIC') set.weight = 50;
|
||||
if (t.type === 'BODYWEIGHT' || t.type === 'STATIC') set.bodyWeightPercentage = 100;
|
||||
if (t.type === 'CARDIO' || t.type === 'STATIC') set.durationSeconds = 60;
|
||||
if (t.type === 'CARDIO' || t.type === 'LONG_JUMP') set.distanceMeters = 100;
|
||||
if (t.type === 'HIGH_JUMP') set.height = 150;
|
||||
return set;
|
||||
});
|
||||
|
||||
|
||||
|
||||
const sessionResp = await request.post('/api/sessions', {
|
||||
data: {
|
||||
startTime: now,
|
||||
endTime: now + 3600000,
|
||||
type: 'STANDARD', // History shows STANDARD sessions differently than QUICK_LOG
|
||||
sets: setsStub
|
||||
},
|
||||
headers: { 'Authorization': `Bearer ${user.token}` }
|
||||
});
|
||||
if (!sessionResp.ok()) {
|
||||
console.log('Session Create Error:', await sessionResp.text());
|
||||
}
|
||||
expect(sessionResp.ok()).toBeTruthy();
|
||||
|
||||
// Go to History
|
||||
await page.getByRole('button', { name: 'History' }).first().click();
|
||||
|
||||
// Find the session card and click Edit (Pencil icon)
|
||||
// There should be only one session
|
||||
await page.locator('.lucide-pencil').first().click();
|
||||
|
||||
await expect(page.getByText('Edit', { exact: true })).toBeVisible();
|
||||
|
||||
// Now verify fields for each exercise in the modal
|
||||
for (const t of types) {
|
||||
const exRow = page.locator('div').filter({ hasText: t.name }).last(); // Find the row for this exercise
|
||||
// This locator might be tricky if the row structure is complex.
|
||||
// In History.tsx:
|
||||
// {editingSession.sets.map((set, idx) => (
|
||||
// <div key={set.id} ...>
|
||||
// ... <span>{set.exerciseName}</span> ...
|
||||
// <div className="grid ..."> inputs here </div>
|
||||
// </div>
|
||||
// ))}
|
||||
|
||||
// So we find the container that has the exercise name, then look for inputs inside it.
|
||||
const row = page.locator('.bg-surface-container-low').filter({ hasText: t.name }).first();
|
||||
await expect(row).toBeVisible();
|
||||
|
||||
console.log(`Checking fields for ${t.type} (${t.name})...`);
|
||||
|
||||
for (const field of t.expectedFields) {
|
||||
// Map field name to label text actually used in History.tsx
|
||||
// t('weight_kg', lang) -> "Weight" (assuming en)
|
||||
// t('reps', lang) -> "Reps"
|
||||
// t('time_sec', lang) -> "Time"
|
||||
// t('dist_m', lang) -> "Distance"
|
||||
// t('height_cm', lang) -> "Height"
|
||||
// t('body_weight_percent', lang) -> "Body Weight %"
|
||||
|
||||
let labelPattern: RegExp;
|
||||
if (field === 'Weight') labelPattern = /Weight/i;
|
||||
else if (field === 'Reps') labelPattern = /Reps/i;
|
||||
else if (field === 'Time') labelPattern = /Time/i;
|
||||
else if (field === 'Distance') labelPattern = /Distance|Dist/i;
|
||||
else if (field === 'Height') labelPattern = /Height/i;
|
||||
else if (field === 'Body Weight') labelPattern = /Body Weight/i;
|
||||
else labelPattern = new RegExp(field, 'i');
|
||||
|
||||
await expect(row.getByLabel(labelPattern).first()).toBeVisible({ timeout: 2000 })
|
||||
.catch(() => { throw new Error(`Missing field '${field}' for type '${t.type}'`); });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user