All Tests Work! Password reset implemented. Users list sorted.

This commit is contained in:
AG
2025-12-10 12:08:11 +02:00
parent bc9b685dec
commit 5597d45e48
16 changed files with 1033 additions and 39 deletions

177
tests/adaptive-gui.spec.ts Normal file
View File

@@ -0,0 +1,177 @@
// spec: specs/gymflow-test-plan.md
import { test, expect } from './fixtures';
test.describe('VI. User Interface & Experience', () => {
test('6.1. A. Adaptive GUI - Mobile Navigation (Width < 768px)', async ({ page, createUniqueUser }) => {
// Note: Use 6.1 numbering as per plan section 6.
const user = await createUniqueUser();
await page.goto('/');
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password);
await page.getByRole('button', { name: 'Login' }).click();
// Handle First Time Password Change
try {
await expect(page.getByRole('heading', { name: /Change Password/i }).or(page.getByText('Free Workout'))).toBeVisible({ timeout: 5000 });
if (await page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
await page.getByLabel('New Password').fill('StrongNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
}
} catch (e) { }
await expect(page.getByText('Free Workout')).toBeVisible();
// 2. Resize the browser window to a mobile width (e.g., 375px).
await page.setViewportSize({ width: 375, height: 667 });
await expect(page.getByText('Free Workout')).toBeVisible();
// 2. Resize the browser window to a mobile width (e.g., 375px).
await page.setViewportSize({ width: 375, height: 667 });
// 3. Verify the bottom navigation bar is visible and functional.
await expect(page.getByRole('navigation', { name: /Bottom|Mobile/i })).toBeVisible();
// Or check for specific mobile nav items if role 'navigation' isn't named.
// Assuming 'Tracker', 'Plans', etc. are visible.
await expect(page.getByRole('button', { name: /Tracker/i })).toBeVisible();
// 4. Verify the desktop navigation rail is hidden.
await expect(page.getByRole('navigation', { name: /Desktop|Side/i })).toBeHidden();
});
test('6.2. A. Adaptive GUI - Desktop Navigation (Width >= 768px)', async ({ page, createUniqueUser }) => {
const user = await createUniqueUser();
await page.goto('/');
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password);
await page.getByRole('button', { name: 'Login' }).click();
try {
await expect(page.getByRole('heading', { name: /Change Password/i }).or(page.getByText('Free Workout'))).toBeVisible({ timeout: 5000 });
if (await page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
await page.getByLabel('New Password').fill('StrongNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
}
} catch (e) { }
await expect(page.getByText('Free Workout')).toBeVisible();
// 1. Resize the browser window to a desktop width (e.g., 1280px).
await page.setViewportSize({ width: 1280, height: 800 });
// 2. Verify the vertical navigation rail is visible and functional.
await expect(page.getByRole('navigation', { name: /Desktop|Side/i })).toBeVisible();
await expect(page.getByRole('button', { name: 'Tracker' })).toBeVisible(); // Check an item
// 3. Verify the mobile bottom navigation bar is hidden.
await expect(page.getByRole('navigation', { name: /Bottom|Mobile/i })).toBeHidden();
});
test('6.3. A. Adaptive GUI - Responsive Charts in Stats', async ({ page, createUniqueUser }) => {
// Using content from adaptive-gui-responsive-charts-in-stats.spec.ts
const user = await createUniqueUser();
await page.goto('/');
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password);
await page.getByRole('button', { name: 'Login' }).click();
try {
await expect(page.getByRole('heading', { name: /Change Password/i }).or(page.getByText('Free Workout'))).toBeVisible({ timeout: 5000 });
if (await page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
await page.getByLabel('New Password').fill('StrongNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
}
} catch (e) { }
await expect(page.getByText('Free Workout')).toBeVisible();
const checkNoHorizontalScroll = async () => {
const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth);
const clientWidth = await page.evaluate(() => document.documentElement.clientWidth);
expect(scrollWidth).toBeLessThanOrEqual(clientWidth);
};
// 1. Navigate to the 'Stats' section.
await page.getByRole('button', { name: 'Stats' }).click();
// Define a range of widths to test responsiveness
const widths = [1280, 1024, 768, 600, 480, 375];
const heights = [800, 768, 667];
for (const width of widths) {
for (const height of heights) {
await page.setViewportSize({ width, height });
// Give time for resize observation/rendering
await page.waitForTimeout(200);
// Check for no overflow
await checkNoHorizontalScroll();
// Check if "Not enough data" is shown
const noData = await page.getByText(/Not enough data/i).isVisible();
if (noData) {
await expect(page.getByText(/Not enough data/i)).toBeVisible();
// Skip chart assertions if no data
} else {
// Verify chart containers are visible
await expect(page.getByRole('heading', { name: /Total Volume/i }).or(page.getByText('Total Volume'))).toBeVisible();
await expect(page.getByRole('heading', { name: /Set Count/i }).or(page.getByText('Set Count'))).toBeVisible();
await expect(page.getByRole('heading', { name: /Body Weight/i }).or(page.getByText('Body Weight'))).toBeVisible();
// Check for presence of SVG or Canvas elements typically used for charts
await expect(page.locator('svg').first()).toBeVisible();
}
}
}
});
test('6.4. A. Adaptive GUI - Fluid Layout Responsiveness', async ({ page, createUniqueUser }) => {
const user = await createUniqueUser();
await page.goto('/');
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password);
await page.getByRole('button', { name: 'Login' }).click();
// Handle First Time Password Change if it appears
try {
await expect(page.getByRole('heading', { name: /Change Password/i }).or(page.getByText('Free Workout'))).toBeVisible({ timeout: 5000 });
if (await page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
await page.getByLabel('New Password').fill('StrongNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
}
} catch (e) {
// Ignore timeout
}
await expect(page.getByText('Free Workout')).toBeVisible();
// Helper to check for horizontal scrollbar (indicates overflow)
const checkNoHorizontalScroll = async () => {
const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth);
const clientWidth = await page.evaluate(() => document.documentElement.clientWidth);
expect(scrollWidth).toBeLessThanOrEqual(clientWidth);
};
// Define a range of widths to test responsiveness
const widths = [1280, 1024, 768, 600, 480, 375];
const heights = [800, 768, 667]; // Corresponding heights
for (const width of widths) {
for (const height of heights) {
await page.setViewportSize({ width, height });
await checkNoHorizontalScroll();
// 1. Navigate through various sections and check responsiveness
await page.getByRole('button', { name: 'Plans' }).click();
await checkNoHorizontalScroll();
await page.getByRole('button', { name: 'Profile' }).click();
await checkNoHorizontalScroll();
await page.getByRole('button', { name: 'History' }).click();
await checkNoHorizontalScroll();
await page.getByRole('button', { name: 'Stats' }).click();
await checkNoHorizontalScroll();
await page.getByRole('button', { name: 'AI Coach' }).click();
await checkNoHorizontalScroll();
await page.getByRole('button', { name: 'Tracker' }).click(); // Go back to default view
await checkNoHorizontalScroll();
}
}
});
});

View File

@@ -0,0 +1,51 @@
// spec: specs/gymflow-test-plan.md
// seed: tests/seed.spec.ts
import { test, expect } from './fixtures';
test.describe('User & System Management', () => {
test('AI Coach - Send a Message', async ({ page, createUniqueUser }) => {
// 1. Log in as a regular user.
const user = await createUniqueUser();
await page.goto('/');
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password);
await page.getByRole('button', { name: 'Login' }).click();
// Handle First Time Password Change if it appears
try {
await expect(page.getByRole('heading', { name: /Change Password/i }).or(page.getByText('Free Workout'))).toBeVisible({ timeout: 5000 });
if (await page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
await page.getByLabel('New Password').fill('StrongNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
}
} catch (e) {
// Ignore timeout
}
await expect(page.getByText('Free Workout')).toBeVisible();
// 2. Navigate to the 'AI Coach' section.
await page.getByRole('button', { name: 'AI Coach' }).click();
// 3. Type a message into the input field (e.g., 'What's a good workout for chest?').
const message = "What's a good workout for chest?";
await page.getByRole('textbox', { name: 'Ask about workouts...' }).fill(message);
// 4. Click 'Send' button.
// Using filter to find the button with no text (icon only) which is the send button in the chat interface
await page.getByRole('button').filter({ hasText: /^$/ }).click();
// Expected Results: User's message appears in the chat.
await expect(page.getByText(message)).toBeVisible();
// Expected Results: AI Coach responds with relevant advice.
// We expect a response to appear. Since AI response takes time, we wait for it.
// We can check for a common response starter or just that another message bubble appears.
// Assuming the response is long, we can check for a part of it or just non-empty text that is NOT the user message.
// Or check if the "thinking" state goes away if implemented.
// Here we'll just wait for any text that contains "chest" or "workout" that isn't the input prompt.
// But better to check for element structure if possible.
// Based on manual execution, we saw "That's a great goal!"
await expect(page.getByText(/chest/i).nth(1)).toBeVisible(); // Just ensuring related content appeared
});
});

View File

@@ -1,10 +1,15 @@
import { test as base, expect } from '@playwright/test';
import { request } from '@playwright/test';
import { exec as cp_exec } from 'child_process';
import { promisify } from 'util';
const exec = promisify(cp_exec);
// Define the type for our custom fixtures
type MyFixtures = {
createUniqueUser: () => Promise<{ email: string, password: string, id: string, token: string }>;
createAdminUser: () => Promise<{ email: string, password: string, id: string, token: string }>;
};
// Extend the base test with our custom fixture
@@ -50,6 +55,34 @@ export const test = base.extend<MyFixtures>({
// Given the requirements "delete own account" exists (5.6), we could theoretically login and delete.
// For now we skip auto-cleanup to keep it simple, assuming test DB is reset periodically.
},
createAdminUser: async ({ createUniqueUser }, use) => {
// Setup: Helper function to create an admin user (create regular -> promote)
const createAdmin = async () => {
const user = await createUniqueUser(); // Create a regular user first
console.log(`Promoting user ${user.email} to ADMIN...`);
try {
const { stdout, stderr } = await exec(`npx ts-node promote_admin.ts ${user.email}`, {
cwd: 'server',
env: { ...process.env, APP_MODE: 'test', DATABASE_URL: 'file:./prisma/test.db', DATABASE_URL_TEST: 'file:./prisma/test.db' }
});
if (stderr) {
console.error(`Promote Admin Stderr: ${stderr}`);
}
console.log(`Promote Admin Stdout: ${stdout}`);
if (!stdout.includes(`User ${user.email} promoted to ADMIN`)) {
throw new Error('Admin promotion failed or unexpected output.');
}
} catch (error) {
console.error(`Error promoting user ${user.email} to ADMIN:`, error);
throw error;
}
return user;
};
await use(createAdmin);
},
});
export { expect };

View File

@@ -1,7 +1,32 @@
import { test, expect } from '@playwright/test';
import { test, expect } from './fixtures';
test.describe('Test group', () => {
test('seed', async ({ page }) => {
// generate code here.
test.describe('Seed', () => {
test('seed', async ({ page, createUniqueUser }) => {
// 1. Create User
const user = await createUniqueUser();
// 2. Go to Login
await page.goto('/');
// 3. Login
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password);
await page.getByRole('button', { name: 'Login' }).click();
// 4. Handle First Time Password Change if it appears
// Wait for either dashboard or change password screen
try {
await expect(page.getByRole('heading', { name: /Change Password/i }).or(page.getByText('Free Workout'))).toBeVisible({ timeout: 5000 });
if (await page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
await page.getByLabel('New Password').fill('StrongNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
}
} catch (e) {
console.log('Timeout waiting for login transition');
}
// 5. Ensure we are at Dashboard
await expect(page.getByText('Free Workout')).toBeVisible();
});
});

View File

@@ -0,0 +1,602 @@
import { test, expect } from './fixtures';
import { exec as cp_exec } from 'child_process';
import { promisify } from 'util';
const exec = promisify(cp_exec);
test.describe('V. User & System Management', () => {
test('5.1. A. User Profile - Update Personal Information', async ({ page, createUniqueUser }) => {
// Seed: Log in as a regular user
const user = await createUniqueUser();
await page.goto('/');
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password);
await page.getByRole('button', { name: 'Login' }).click();
// Handle potential first-time login
try {
await expect(page.getByRole('heading', { name: /Change Password/i }).or(page.getByText('Free Workout'))).toBeVisible({ timeout: 5000 });
if (await page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
await page.getByLabel('New Password').fill('StrongNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
}
} catch (e) {
// Ignore timeout if it proceeds
}
await expect(page.getByText('Free Workout')).toBeVisible();
// 2. Navigate to the 'Profile' section.
await page.getByRole('button', { name: 'Profile' }).click();
// 3. Modify 'Weight', 'Height', 'Birth Date', and 'Gender'.
await page.getByTestId('profile-weight-input').fill('75');
await page.getByTestId('profile-height-input').fill('180');
await page.getByTestId('profile-birth-date').fill('1990-01-01');
await page.getByTestId('profile-gender').selectOption('FEMALE');
// 4. Click 'Save Profile'.
await page.getByRole('button', { name: 'Save Profile' }).click();
await expect(page.getByText('Profile saved successfully')).toBeVisible();
// Verify persistence
await page.reload();
// After reload, we might be on dashboard or profile depending on app routing, but let's ensure we go to profile
if (!await page.getByRole('heading', { name: 'Profile' }).isVisible()) {
await page.getByRole('button', { name: 'Profile' }).click();
}
// Verify values
await expect(page.getByTestId('profile-weight-input')).toHaveValue('75');
await expect(page.getByTestId('profile-height-input')).toHaveValue('180');
await expect(page.getByTestId('profile-birth-date')).toHaveValue('1990-01-01');
await expect(page.getByTestId('profile-gender')).toHaveValue('FEMALE');
});
test('5.2. A. User Profile - Change Password', async ({ page, createUniqueUser }) => {
// Seed: Log in as a regular user
const user = await createUniqueUser();
await page.goto('/');
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password);
await page.getByRole('button', { name: 'Login' }).click();
// Handle potential first-time login
try {
await expect(page.getByRole('heading', { name: /Change Password/i }).or(page.getByText('Free Workout'))).toBeVisible({ timeout: 5000 });
if (await page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
await page.getByLabel('New Password').fill('StrongNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
}
} catch (e) {
// Ignore timeout
}
await expect(page.getByText('Free Workout')).toBeVisible();
// 2. Navigate to the 'Profile' section.
await page.getByRole('button', { name: 'Profile' }).click();
// 3. Enter a new password (min 4 characters) in the 'Change Password' field.
const newPassword = 'NewStrongPass!';
await page.getByRole('textbox', { name: 'New Password' }).fill(newPassword);
// 4. Click 'OK'.
await page.getByRole('button', { name: 'OK' }).click();
await expect(page.getByText('Password changed')).toBeVisible();
// Verify: The user can log in with the new password.
// Logout first
await page.getByRole('button', { name: 'Logout' }).click();
// Login with new password
await page.getByRole('textbox', { name: 'Email' }).fill(user.email);
await page.getByRole('textbox', { name: 'Password' }).fill(newPassword);
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.getByText('Free Workout')).toBeVisible();
});
test('5.3. A. User Profile - Change Password (Too Short)', async ({ page, createUniqueUser }) => {
// Seed
const user = await createUniqueUser();
await page.goto('/');
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password);
await page.getByRole('button', { name: 'Login' }).click();
try {
await expect(page.getByRole('heading', { name: /Change Password/i }).or(page.getByText('Free Workout'))).toBeVisible({ timeout: 5000 });
if (await page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
await page.getByLabel('New Password').fill('StrongNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
}
} catch (e) { }
await expect(page.getByText('Free Workout')).toBeVisible();
// 2. Navigate to Profile
await page.getByRole('button', { name: 'Profile' }).click();
// 3. Enter short password
await page.getByRole('textbox', { name: 'New Password' }).fill('123');
// 4. Click OK
await page.getByRole('button', { name: 'OK' }).click();
// Expect Error
await expect(page.getByText('Password too short')).toBeVisible();
});
test('5.4. A. User Profile - Dedicated Daily Weight Logging', async ({ page, createUniqueUser }) => {
// Seed
const user = await createUniqueUser();
await page.goto('/');
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password);
await page.getByRole('button', { name: 'Login' }).click();
try {
await expect(page.getByRole('heading', { name: /Change Password/i }).or(page.getByText('Free Workout'))).toBeVisible({ timeout: 5000 });
if (await page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
await page.getByLabel('New Password').fill('StrongNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
}
} catch (e) { }
await expect(page.getByText('Free Workout')).toBeVisible();
// 2. Navigate to Profile
await page.getByRole('button', { name: 'Profile' }).click();
// 3. Expand Weight Tracker
await page.getByRole('button', { name: 'Weight Tracker' }).click();
// 4. Enter weight
const weight = '72.3';
await page.getByPlaceholder('Enter weight...').fill(weight);
// 5. Click Log
await page.getByRole('button', { name: 'Log', exact: true }).click();
// Expect success message
await expect(page.getByText('Weight logged successfully')).toBeVisible();
// Expect record in history
await expect(page.getByText(`${weight} kg`)).toBeVisible();
// Check if profile weight updated
await expect(page.getByRole('spinbutton').first()).toHaveValue(weight);
});
test('5.5. A. User Profile - Language Preference Change', async ({ page, createUniqueUser }) => {
// 1. Log in as a regular user.
const user = await createUniqueUser();
await page.goto('/');
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password);
await page.getByRole('button', { name: 'Login' }).click();
// Handle First Time Password Change if it appears
try {
await expect(page.getByRole('heading', { name: /Change Password/i }).or(page.getByText('Free Workout'))).toBeVisible({ timeout: 5000 });
if (await page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
await page.getByLabel('New Password').fill('StrongNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
}
} catch (e) {
// Ignore timeout
}
await expect(page.getByText('Free Workout')).toBeVisible();
// 2. Navigate to the 'Profile' section.
await page.getByRole('button', { name: 'Profile' }).click();
// 3. Select a different language (e.g., 'Русский') from the language dropdown.
await page.getByRole('combobox').nth(1).selectOption(['ru']); // Value is 'ru'
// 4. Click 'Save Profile'.
await page.getByRole('button', { name: /Сохранить профиль|Save Profile/ }).click();
// Expected Results: The UI language immediately switches to the selected language.
await expect(page.getByRole('heading', { name: 'Профиль', exact: true })).toBeVisible();
await expect(page.getByText(/Profile saved|Профиль успешно/)).toBeVisible(); // Wait for persistence
await expect(page.getByRole('button', { name: 'Сохранить профиль' })).toBeVisible();
// Expected Results: The preference persists across sessions.
await page.reload();
// Check if we are still logged in or need to login
if (await page.getByLabel('Email').isVisible()) {
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password || 'StrongNewPass123!');
await page.getByRole('button', { name: 'Login' }).click();
}
// Verify language is still Russian
await page.getByRole('button', { name: /Профиль|Profile/ }).click();
await expect(page.getByRole('heading', { name: 'Профиль', exact: true })).toBeVisible();
});
test('5.6. A. User Profile - Delete Own Account', async ({ page, createUniqueUser }) => {
const user = await createUniqueUser();
await page.goto('/');
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password);
await page.getByRole('button', { name: 'Login' }).click();
try {
await expect(page.getByRole('heading', { name: /Change Password/i }).or(page.getByText('Free Workout'))).toBeVisible({ timeout: 5000 });
if (await page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
await page.getByLabel('New Password').fill('StrongNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
}
} catch (e) { }
await expect(page.getByText('Free Workout')).toBeVisible();
await page.getByRole('button', { name: 'Profile' }).click();
await page.getByRole('button', { name: 'Delete' }).click();
await expect(page.getByText('Are you sure?')).toBeVisible();
await page.getByRole('button', { name: 'Delete', exact: true }).last().click();
await expect(page).toHaveURL(/\/login/);
await expect(page.getByRole('button', { name: 'Login' })).toBeVisible();
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password || 'StrongNewPass123!');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.getByText(/Invalid credentials|User not found/i)).toBeVisible();
});
// --- Admin Panel Tests ---
test('5.7. B. Admin Panel - View User List', async ({ page, createAdminUser, request }) => {
test.setTimeout(120000); // Extend timeout for multiple user creation
const adminUser = await createAdminUser();
// Create 25 users to populate the list using Promise.all for parallelism
const createdEmails: string[] = [];
const creationPromises = [];
for (let i = 0; i < 25; i++) {
const uniqueId = Math.random().toString(36).substring(7);
const email = `list.user.${i}.${uniqueId}@example.com`;
const password = 'StrongPassword123!';
createdEmails.push(email);
creationPromises.push(request.post('/api/auth/register', {
data: { email, password }
}));
}
const responses = await Promise.all(creationPromises);
for (const response of responses) {
await expect(response).toBeOK();
}
await page.goto('/');
await page.getByLabel('Email').fill(adminUser.email);
await page.getByLabel('Password').fill(adminUser.password);
await page.getByRole('button', { name: 'Login' }).click();
try {
await expect(page.getByRole('heading', { name: /Change Password/i }).or(page.getByText('Free Workout'))).toBeVisible({ timeout: 5000 });
if (await page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
await page.getByLabel('New Password').fill('StrongAdminNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
}
} catch (e) { }
await expect(page.getByText('Free Workout')).toBeVisible();
await page.getByRole('button', { name: 'Profile' }).click();
// Expand Users List (Admin Area is a header)
await page.getByRole('button', { name: /Users List|User List/i }).click();
await expect(page.getByText(/Users List/i)).toBeVisible();
// Verify all created users are visible in the list
for (const email of createdEmails) {
await expect(page.getByText(email)).toBeVisible();
}
});
test('5.8. B. Admin Panel - Create New User', async ({ page, createAdminUser }) => {
const adminUser = await createAdminUser();
await page.goto('/');
await page.getByLabel('Email').fill(adminUser.email);
await page.getByLabel('Password').fill(adminUser.password);
await page.getByRole('button', { name: 'Login' }).click();
try {
await expect(page.getByRole('heading', { name: /Change Password/i }).or(page.getByText('Free Workout'))).toBeVisible({ timeout: 5000 });
if (await page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
await page.getByLabel('New Password').fill('StrongAdminNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
}
} catch (e) { }
await expect(page.getByText('Free Workout')).toBeVisible();
await page.getByRole('button', { name: 'Profile' }).click();
const uniqueId = Math.random().toString(36).substring(7);
const newUserEmail = `new.user.${uniqueId}@example.com`;
const newUserPassword = 'NewUserPass123!';
const createUserSection = page.locator('div').filter({ has: page.getByRole('heading', { name: 'Create User' }) }).last();
await createUserSection.getByLabel('Email').fill(newUserEmail);
await createUserSection.getByLabel('Password').fill(newUserPassword);
await page.getByRole('button', { name: /Create User|Create/i }).click();
await expect(page.getByText(/User created|successfully/i)).toBeVisible();
const userListButton = page.getByRole('button', { name: /Users List/i });
if (await userListButton.getAttribute('aria-expanded') !== 'true') {
await userListButton.click();
}
const listContainer = page.locator('div.space-y-4.mt-4');
await expect(listContainer).toBeVisible();
await expect(listContainer.getByText(newUserEmail)).toBeVisible();
});
test('5.9. B. Admin Panel - Block/Unblock User', async ({ page, createAdminUser, createUniqueUser }) => {
const adminUser = await createAdminUser();
// 1. Login as Admin
await page.goto('/');
await page.getByLabel('Email').fill(adminUser.email);
await page.getByLabel('Password').fill(adminUser.password);
await page.getByRole('button', { name: 'Login' }).click();
try {
await expect(page.getByRole('heading', { name: /Change Password/i }).or(page.getByText('Free Workout'))).toBeVisible({ timeout: 5000 });
if (await page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
await page.getByLabel('New Password').fill('StrongAdminNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
}
} catch (e) { }
await expect(page.getByText('Free Workout')).toBeVisible();
console.log('Logged in as Admin');
// 2. Create a Regular User (via API)
const regularUser = await createUniqueUser();
console.log('Regular user created:', regularUser.email);
// 3. Navigate to Admin Panel -> User List
await page.getByRole('button', { name: 'Profile' }).filter({ visible: true }).click();
// Ensure list is open and valid
const userListButton = page.getByRole('button', { name: /Users List/i });
// Check expanded state and Open if currently closed
const isExpanded = await userListButton.getAttribute('aria-expanded');
if (isExpanded !== 'true') {
await userListButton.click();
}
await expect(userListButton).toHaveAttribute('aria-expanded', 'true');
console.log('User list is open');
// Always Refresh to ensure latest users are fetched
await Promise.all([
page.waitForResponse(resp => resp.url().includes('/auth/users')),
page.getByTitle('Refresh List').click()
]);
// Ensure list remained open or re-open it
if (await userListButton.getAttribute('aria-expanded') !== 'true') {
console.log('List closed after refresh, re-opening...');
await userListButton.click();
}
// Verify user row exists
// Fallback to CSS selector if data-testid is missing due to build issues
const listContainer = page.locator('div.space-y-4.mt-4');
await expect(listContainer).toBeVisible();
const userRow = listContainer.locator('.bg-surface-container-high').filter({ hasText: regularUser.email }).first();
await expect(userRow).toBeVisible();
// 4. Block the User
// Use exact name matching or title since we added aria-label
const blockButton = userRow.getByRole('button', { name: 'Block', exact: true });
if (await blockButton.count() === 0) {
console.log('Block button NOT found!');
// fallback to find any button to see what is there
const buttons = await userRow.getByRole('button').all();
console.log('Buttons found in row:', buttons.length);
}
await expect(blockButton).toBeVisible();
await blockButton.click();
await expect(userRow.getByText(/Blocked|Block/i)).toBeVisible();
// 5. Verify Blocked User Cannot Login
// Logout Admin
const logoutButton = page.getByRole('button', { name: /Logout/i });
if (await logoutButton.isVisible()) {
await logoutButton.click();
} else {
await page.getByText(/Logout/i).click();
}
await expect(page.getByRole('button', { name: 'Login' })).toBeVisible();
// Attempt Login as Blocked User
await page.getByLabel('Email').fill(regularUser.email);
await page.getByLabel('Password').fill(regularUser.password);
await page.getByRole('button', { name: 'Login' }).click();
// Assert Error Message
await expect(page.getByText(/Account is blocked/i)).toBeVisible();
// 6. Unblock the User
// Reload to clear any error states/toasts from previous attempt
await page.reload();
// Login as Admin again
await page.getByLabel('Email').fill(adminUser.email);
// Force the new password since we know step 1 changed it
await page.getByLabel('Password').fill('StrongAdminNewPass123!');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.getByText('Free Workout')).toBeVisible();
console.log('Admin logged back in');
await page.waitForTimeout(1000);
await page.getByRole('button', { name: 'Profile' }).filter({ visible: true }).click();
// Open list again
await userListButton.click();
await page.getByTitle('Refresh List').click();
// Unblock
const userRowAfter = listContainer.locator('.bg-surface-container-high').filter({ hasText: regularUser.email }).first();
await expect(userRowAfter).toBeVisible();
await userRowAfter.getByRole('button', { name: 'Unblock', exact: true }).click();
// Wait for UI to update (block icon/text should disappear or change style)
// Ideally we check API response or UI change. Assuming "Blocked" text goes away or button changes.
// The original code checked for not.toBeVisible of blocked text, let's stick to that or button state
await expect(userRowAfter.getByText(/Blocked/i)).not.toBeVisible();
// 7. Verify Unblocked User Can Login
await page.getByRole('button', { name: 'Logout' }).click();
await page.getByLabel('Email').fill(regularUser.email);
await page.getByLabel('Password').fill(regularUser.password);
await page.getByRole('button', { name: 'Login' }).click();
// Check for Change Password (first login) or direct Dashboard
await expect(page.getByRole('heading', { name: /Change Password/i }).or(page.getByText('Free Workout'))).toBeVisible({ timeout: 5000 });
if (await page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
await page.getByLabel('New Password').fill('StrongUserNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
}
await expect(page.getByText('Free Workout')).toBeVisible();
});
test('5.10. B. Admin Panel - Reset User Password', async ({ page, createAdminUser, createUniqueUser }) => {
const adminUser = await createAdminUser();
await page.goto('/');
await page.getByLabel('Email').fill(adminUser.email);
await page.getByLabel('Password').fill(adminUser.password);
await page.getByRole('button', { name: 'Login' }).click();
try {
await expect(page.getByRole('heading', { name: /Change Password/i }).or(page.getByText('Free Workout'))).toBeVisible({ timeout: 5000 });
if (await page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
await page.getByLabel('New Password').fill('StrongAdminNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
}
} catch (e) { }
const regularUser = await createUniqueUser();
const newPassword = 'NewStrongUserPass!';
await page.getByRole('button', { name: 'Profile' }).click();
// Ensure list is open and valid (Reusing logic from 5.9)
const userListButton = page.getByRole('button', { name: /Users List/i });
const isExpanded = await userListButton.getAttribute('aria-expanded');
if (isExpanded !== 'true') {
await userListButton.click();
}
await expect(userListButton).toHaveAttribute('aria-expanded', 'true');
// Always Refresh to ensure latest users are fetched
await Promise.all([
page.waitForResponse(resp => resp.url().includes('/auth/users')),
page.getByTitle('Refresh List').click()
]);
// Ensure list remained open
if (await userListButton.getAttribute('aria-expanded') !== 'true') {
await userListButton.click();
}
const listContainer = page.locator('div.space-y-4.mt-4');
await expect(listContainer).toBeVisible();
const userRow = listContainer.locator('.bg-surface-container-high').filter({ hasText: regularUser.email }).first();
await expect(userRow).toBeVisible();
await userRow.getByRole('textbox').fill(newPassword);
page.on('dialog', async dialog => {
console.log(`Dialog message: ${dialog.message()}`);
await dialog.accept();
});
await userRow.getByRole('button', { name: /Reset Pass/i }).click();
// Wait to ensure the operation completed (the dialog is the signal, but we might need a small buffer or check effect)
// Since dialog is handled immediately by listener, we might race.
// Better pattern: wait for the button to be enabled again or some UI feedback.
// But since we use window.alert, expecting the dialog content is tricky in Playwright if not careful.
// Let's add a small pause to allow backend to process before logout.
await page.waitForTimeout(1000);
await page.getByRole('button', { name: 'Logout' }).click();
await page.getByLabel('Email').fill(regularUser.email);
await page.getByLabel('Password').fill(newPassword);
await page.getByRole('button', { name: 'Login' }).click();
// After reset, isFirstLogin is true, so we expect Change Password screen
await expect(page.getByRole('heading', { name: /Change Password/i })).toBeVisible({ timeout: 10000 });
await page.getByLabel('New Password').fill('BrandNewUserPass1!');
await page.getByRole('button', { name: /Save|Change/i }).click();
await expect(page.getByText('Free Workout')).toBeVisible();
});
test('5.11. B. Admin Panel - Delete User', async ({ page, createAdminUser, createUniqueUser }) => {
const adminUser = await createAdminUser();
await page.goto('/');
await page.getByLabel('Email').fill(adminUser.email);
await page.getByLabel('Password').fill(adminUser.password);
await page.getByRole('button', { name: 'Login' }).click();
try {
await expect(page.getByRole('heading', { name: /Change Password/i }).or(page.getByText('Free Workout'))).toBeVisible({ timeout: 5000 });
if (await page.getByRole('heading', { name: /Change Password/i }).isVisible()) {
await page.getByLabel('New Password').fill('StrongAdminNewPass123!');
await page.getByRole('button', { name: /Save|Change/i }).click();
}
} catch (e) { }
const userToDelete = await createUniqueUser();
await page.getByRole('button', { name: 'Profile' }).click();
// Ensure list is open and valid (Reusing logic from 5.9)
const userListButton = page.getByRole('button', { name: /Users List/i });
const isExpanded = await userListButton.getAttribute('aria-expanded');
if (isExpanded !== 'true') {
await userListButton.click();
}
await expect(userListButton).toHaveAttribute('aria-expanded', 'true');
// Always Refresh to ensure latest users are fetched
await Promise.all([
page.waitForResponse(resp => resp.url().includes('/auth/users')),
page.getByTitle('Refresh List').click()
]);
// Ensure list remained open
if (await userListButton.getAttribute('aria-expanded') !== 'true') {
await userListButton.click();
}
const listContainer = page.locator('div.space-y-4.mt-4');
await expect(listContainer).toBeVisible();
const userRow = listContainer.locator('.bg-surface-container-high').filter({ hasText: userToDelete.email }).first();
await expect(userRow).toBeVisible();
page.once('dialog', dialog => dialog.accept());
await userRow.getByRole('button', { name: /Delete/i }).click();
await expect(page.getByText(userToDelete.email)).not.toBeVisible();
});
});