test: add playwright config and e2e tests
This commit is contained in:
@@ -0,0 +1,38 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('Auth guard', () => {
|
||||||
|
test('redirects to /login when not authenticated', async ({ page }) => {
|
||||||
|
// Ensure no auth state
|
||||||
|
await page.goto('/login');
|
||||||
|
await page.evaluate(() => localStorage.removeItem('isLoggedIn'));
|
||||||
|
await page.goto('/welcome');
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/\/login/);
|
||||||
|
await expect(page.locator('.login-page')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('redirects / to /login when not authenticated', async ({ page }) => {
|
||||||
|
await page.goto('/login');
|
||||||
|
await page.evaluate(() => localStorage.removeItem('isLoggedIn'));
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/\/login/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('allows access to /welcome when authenticated', async ({ page }) => {
|
||||||
|
await page.route('**/api/review/feed', (route) =>
|
||||||
|
route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({ code: 200, data: [] }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.goto('/login');
|
||||||
|
await page.evaluate(() => localStorage.setItem('isLoggedIn', 'true'));
|
||||||
|
await page.goto('/welcome');
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/\/welcome/);
|
||||||
|
await expect(page.locator('.welcome-page')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
const mockLoginSuccess = { code: 200, message: '登录成功' };
|
||||||
|
const mockLoginFailure = { code: 401, message: '账号或密码错误' };
|
||||||
|
const mockTasks = { code: 200, data: { records: [] } };
|
||||||
|
const mockFeed = { code: 200, data: [] };
|
||||||
|
|
||||||
|
test.describe('Login page', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/login');
|
||||||
|
await page.waitForSelector('.login-page', { timeout: 10_000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders the login form with title and inputs', async ({ page }) => {
|
||||||
|
await expect(page.locator('h2')).toContainText('账号登录');
|
||||||
|
await expect(page.locator('.login-form')).toBeVisible();
|
||||||
|
// Element Plus inputs
|
||||||
|
const inputs = page.locator('.el-input');
|
||||||
|
await expect(inputs).toHaveCount(2);
|
||||||
|
await expect(page.locator('.submit-btn')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows validation errors when fields are empty', async ({ page }) => {
|
||||||
|
await page.locator('.submit-btn').click();
|
||||||
|
// Element Plus validation shows error messages
|
||||||
|
await expect(page.locator('.el-form-item__error').first()).toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows error on invalid credentials', async ({ page }) => {
|
||||||
|
await page.route('**/api/login**', (route) =>
|
||||||
|
route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify(mockLoginFailure),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await page.route('**/api/tasks**', (route) =>
|
||||||
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockTasks) }),
|
||||||
|
);
|
||||||
|
await page.route('**/api/review/feed**', (route) =>
|
||||||
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockFeed) }),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fill in inputs using Element Plus el-input inner input
|
||||||
|
const usernameInput = page.locator('.el-form-item').filter({ hasText: '账号' }).locator('input');
|
||||||
|
const passwordInput = page.locator('.el-form-item').filter({ hasText: '密码' }).locator('input');
|
||||||
|
await usernameInput.fill('wronguser');
|
||||||
|
await passwordInput.fill('wrongpass');
|
||||||
|
await page.locator('.submit-btn').click();
|
||||||
|
|
||||||
|
// Element Plus error message appears in a popup
|
||||||
|
await expect(page.locator('.el-message--error')).toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('successful login navigates to /welcome', async ({ page }) => {
|
||||||
|
await page.route('**/api/login**', (route) =>
|
||||||
|
route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify(mockLoginSuccess),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await page.route('**/api/tasks**', (route) =>
|
||||||
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockTasks) }),
|
||||||
|
);
|
||||||
|
await page.route('**/api/review/feed**', (route) =>
|
||||||
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockFeed) }),
|
||||||
|
);
|
||||||
|
|
||||||
|
const usernameInput = page.locator('.el-form-item').filter({ hasText: '账号' }).locator('input');
|
||||||
|
const passwordInput = page.locator('.el-form-item').filter({ hasText: '密码' }).locator('input');
|
||||||
|
await usernameInput.fill('admin');
|
||||||
|
await passwordInput.fill('password123');
|
||||||
|
await page.locator('.submit-btn').click();
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/\/welcome/, { timeout: 10_000 });
|
||||||
|
const isLoggedIn = await page.evaluate(() => localStorage.getItem('isLoggedIn'));
|
||||||
|
expect(isLoggedIn).toBe('true');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
const mockTasks = { code: 200, data: { records: [] } };
|
||||||
|
const mockFeed = { code: 200, data: [] };
|
||||||
|
|
||||||
|
test.describe('Header navigation', () => {
|
||||||
|
test('shows brand title on login page', async ({ page }) => {
|
||||||
|
await page.goto('/login');
|
||||||
|
await expect(page.locator('.login-page')).toBeVisible({ timeout: 10_000 });
|
||||||
|
await expect(page.locator('.login-panel h1')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows logout button on authenticated pages', async ({ page }) => {
|
||||||
|
await page.route('**/api/review/feed**', (route) =>
|
||||||
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockFeed) }),
|
||||||
|
);
|
||||||
|
await page.goto('/login');
|
||||||
|
await page.evaluate(() => localStorage.setItem('isLoggedIn', 'true'));
|
||||||
|
await page.goto('/welcome');
|
||||||
|
await page.waitForSelector('.welcome-page', { timeout: 10_000 });
|
||||||
|
await expect(page.locator('.head .logout-btn')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('logout clears auth and redirects to /login', async ({ page }) => {
|
||||||
|
await page.route('**/api/review/feed**', (route) =>
|
||||||
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockFeed) }),
|
||||||
|
);
|
||||||
|
await page.goto('/login');
|
||||||
|
await page.evaluate(() => localStorage.setItem('isLoggedIn', 'true'));
|
||||||
|
await page.goto('/welcome');
|
||||||
|
await page.waitForSelector('.welcome-page', { timeout: 10_000 });
|
||||||
|
await page.locator('.head .logout-btn').click();
|
||||||
|
await expect(page).toHaveURL(/\/login/, { timeout: 5000 });
|
||||||
|
const isLoggedIn = await page.evaluate(() => localStorage.getItem('isLoggedIn'));
|
||||||
|
expect(isLoggedIn).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('back button shows on sub-pages and navigates to /welcome', async ({ page }) => {
|
||||||
|
await page.route('**/api/tasks**', (route) =>
|
||||||
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockTasks) }),
|
||||||
|
);
|
||||||
|
await page.route('**/api/review/feed**', (route) =>
|
||||||
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockFeed) }),
|
||||||
|
);
|
||||||
|
await page.goto('/login');
|
||||||
|
await page.evaluate(() => localStorage.setItem('isLoggedIn', 'true'));
|
||||||
|
await page.goto('/study');
|
||||||
|
await page.waitForSelector('.study-page', { timeout: 10_000 });
|
||||||
|
await expect(page.locator('.head .back-btn')).toBeVisible();
|
||||||
|
await page.locator('.head .back-btn').click();
|
||||||
|
await expect(page).toHaveURL(/\/welcome/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no back button on welcome page', async ({ page }) => {
|
||||||
|
await page.route('**/api/review/feed**', (route) =>
|
||||||
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockFeed) }),
|
||||||
|
);
|
||||||
|
await page.goto('/login');
|
||||||
|
await page.evaluate(() => localStorage.setItem('isLoggedIn', 'true'));
|
||||||
|
await page.goto('/welcome');
|
||||||
|
await page.waitForSelector('.welcome-page', { timeout: 10_000 });
|
||||||
|
await expect(page.locator('.head .back-btn')).not.toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
const mockTasksData = {
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
taskNum: 'T001',
|
||||||
|
taskName: '学习 Vue3',
|
||||||
|
reportCount: 3,
|
||||||
|
fragmentCount: 5,
|
||||||
|
effectiveTime: '2h 30m',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
taskNum: 'T002',
|
||||||
|
taskName: '学习 TypeScript',
|
||||||
|
reportCount: 1,
|
||||||
|
fragmentCount: 2,
|
||||||
|
effectiveTime: '1h 15m',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const mockTasksEmpty = { code: 200, data: { records: [] } };
|
||||||
|
const mockFeed = { code: 200, data: [] };
|
||||||
|
|
||||||
|
test.describe('Review page', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.route('**/api/tasks**', (route) =>
|
||||||
|
route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify(mockTasksData),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await page.route('**/api/review/feed**', (route) =>
|
||||||
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockFeed) }),
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.goto('/login');
|
||||||
|
await page.evaluate(() => localStorage.setItem('isLoggedIn', 'true'));
|
||||||
|
await page.goto('/review');
|
||||||
|
await page.waitForSelector('.review-page', { timeout: 10_000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('displays summary metrics and task table', async ({ page }) => {
|
||||||
|
await expect(page.locator('.metric-card')).toHaveCount(3);
|
||||||
|
// Check metric values
|
||||||
|
await expect(page.locator('.metric-card').first().locator('strong')).toContainText('2');
|
||||||
|
// Check table rows
|
||||||
|
await expect(page.locator('.el-table__row')).toHaveCount(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows empty state when no tasks', async ({ page }) => {
|
||||||
|
await page.route('**/api/tasks**', (route) =>
|
||||||
|
route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify(mockTasksEmpty),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.goto('/review');
|
||||||
|
await page.waitForSelector('.review-page', { timeout: 10_000 });
|
||||||
|
await expect(page.locator('.metric-card').first().locator('strong')).toContainText('0');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
const mockTasks = {
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
taskNum: 'T001',
|
||||||
|
taskName: '学习 Vue3 组合式 API',
|
||||||
|
taskDescription: '掌握 Composition API 的核心概念',
|
||||||
|
taskPriority: 5,
|
||||||
|
materialUrl: 'https://vuejs.org/guide',
|
||||||
|
lastLearningStatus: '进行中',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
taskNum: 'T002',
|
||||||
|
taskName: 'TypeScript 类型体操',
|
||||||
|
taskDescription: '练习 TypeScript 高级类型',
|
||||||
|
taskPriority: 3,
|
||||||
|
materialUrl: '',
|
||||||
|
lastLearningStatus: '未开始',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const mockTasksEmpty = { code: 200, data: { records: [] } };
|
||||||
|
const mockFeed = { code: 200, data: [] };
|
||||||
|
|
||||||
|
const authAndNavigate = async (page: any) => {
|
||||||
|
await page.goto('/login');
|
||||||
|
await page.evaluate(() => localStorage.setItem('isLoggedIn', 'true'));
|
||||||
|
};
|
||||||
|
|
||||||
|
test.describe('Study page', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.route('**/api/tasks**', (route) => {
|
||||||
|
const url = route.request().url();
|
||||||
|
if (url.includes('/tasks') && !url.includes('/tasks/')) {
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify(mockTasks),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({ code: 200, data: {} }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await page.route('**/api/review/feed**', (route) =>
|
||||||
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockFeed) }),
|
||||||
|
);
|
||||||
|
|
||||||
|
await authAndNavigate(page);
|
||||||
|
await page.goto('/study');
|
||||||
|
await page.waitForSelector('.study-page', { timeout: 10_000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('displays task list with names and priorities', async ({ page }) => {
|
||||||
|
const taskItems = page.locator('.task-list-item');
|
||||||
|
await expect(taskItems).toHaveCount(2);
|
||||||
|
await expect(taskItems.first().locator('.task-name')).toContainText('学习 Vue3 组合式 API');
|
||||||
|
await expect(taskItems.nth(1).locator('.task-name')).toContainText('TypeScript 类型体操');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('selecting a task shows its details', async ({ page }) => {
|
||||||
|
const taskItems = page.locator('.task-list-item');
|
||||||
|
await taskItems.nth(1).click();
|
||||||
|
await expect(page.locator('.detail-head h3')).toContainText('TypeScript 类型体操');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clicking add navigates to add-task form', async ({ page }) => {
|
||||||
|
await page.locator('.action-card').locator('button', { hasText: '添加任务' }).click();
|
||||||
|
await expect(page).toHaveURL(/\/add-task/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clicking update navigates to update-task form', async ({ page }) => {
|
||||||
|
await page.locator('.action-card').locator('button', { hasText: '更新任务' }).click();
|
||||||
|
await expect(page).toHaveURL(/\/update-task/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('delete task removes it from the list', async ({ page }) => {
|
||||||
|
await page.route('**/api/tasks/1', (route) =>
|
||||||
|
route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({ code: 200, message: '删除成功' }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.locator('.action-card').locator('button', { hasText: '删除任务' }).click();
|
||||||
|
await expect(page.locator('.task-list-item')).toHaveCount(1, { timeout: 5000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows empty state when no tasks', async ({ page }) => {
|
||||||
|
await page.route('**/api/tasks**', (route) =>
|
||||||
|
route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify(mockTasksEmpty),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.goto('/study');
|
||||||
|
await page.waitForSelector('.study-page', { timeout: 10_000 });
|
||||||
|
await expect(page.locator('.el-empty')).toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
const mockFeed = { code: 200, data: [] };
|
||||||
|
|
||||||
|
const loginAndGo = async (page: any, url: string) => {
|
||||||
|
await page.goto('/login');
|
||||||
|
await page.evaluate(() => localStorage.setItem('isLoggedIn', 'true'));
|
||||||
|
await page.goto(url);
|
||||||
|
await page.waitForSelector('.task-form-page', { timeout: 10_000 });
|
||||||
|
};
|
||||||
|
|
||||||
|
test.describe('Task form', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.route('**/api/review/feed**', (route) =>
|
||||||
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockFeed) }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('add-task form renders empty fields and "添加" button', async ({ page }) => {
|
||||||
|
await loginAndGo(page, '/add-task');
|
||||||
|
await expect(page.locator('.el-form-item').filter({ hasText: '任务名称' }).locator('input')).toHaveValue('');
|
||||||
|
await expect(page.locator('.el-form-item').filter({ hasText: '任务描述' }).locator('textarea')).toHaveValue('');
|
||||||
|
await expect(page.locator('.action-row button').first()).toContainText('添加');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('submitting add-task sends POST and navigates to /study', async ({ page }) => {
|
||||||
|
await page.route('**/api/tasks', (route) => {
|
||||||
|
if (route.request().method() === 'POST') {
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({ code: 200, message: '创建成功' }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 200, data: { records: [] } }) });
|
||||||
|
});
|
||||||
|
await loginAndGo(page, '/add-task');
|
||||||
|
|
||||||
|
await page.locator('.el-form-item').filter({ hasText: '任务名称' }).locator('input').fill('新学习任务');
|
||||||
|
await page.locator('.el-form-item').filter({ hasText: '任务描述' }).locator('textarea').fill('这是一个测试任务');
|
||||||
|
await page.locator('.action-row button').first().click();
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/\/study/, { timeout: 10_000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('update-task loads existing data and submits PUT', async ({ page }) => {
|
||||||
|
const mockTask = {
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
id: 1,
|
||||||
|
taskName: '已有任务',
|
||||||
|
taskDescription: '已有描述',
|
||||||
|
materialURL: 'https://example.com',
|
||||||
|
urgency: 3,
|
||||||
|
importance: 4,
|
||||||
|
contentDifficulty: 2,
|
||||||
|
futureValue: 5,
|
||||||
|
subjectivePriority: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await page.route('**/api/tasks/1', (route) => {
|
||||||
|
if (route.request().method() === 'PUT') {
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({ code: 200, message: '更新成功' }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify(mockTask),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await loginAndGo(page, '/update-task/1');
|
||||||
|
await expect(page.locator('.el-form-item').filter({ hasText: '任务名称' }).locator('input')).toHaveValue('已有任务');
|
||||||
|
await expect(page.locator('.action-row button').first()).toContainText('更新');
|
||||||
|
|
||||||
|
await page.locator('.el-form-item').filter({ hasText: '任务名称' }).locator('input').fill('更新后的任务');
|
||||||
|
await page.locator('.action-row button').first().click();
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/\/study/, { timeout: 10_000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cancel navigates back to /study', async ({ page }) => {
|
||||||
|
await loginAndGo(page, '/add-task');
|
||||||
|
await page.locator('.action-row button').nth(1).click();
|
||||||
|
await expect(page).toHaveURL(/\/study/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('Welcome page', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.route('**/api/review/feed', (route) =>
|
||||||
|
route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({ code: 200, data: [] }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.goto('/login');
|
||||||
|
await page.evaluate(() => localStorage.setItem('isLoggedIn', 'true'));
|
||||||
|
await page.goto('/welcome');
|
||||||
|
await page.waitForSelector('.welcome-page', { timeout: 10_000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows welcome banner and quick action cards', async ({ page }) => {
|
||||||
|
await expect(page.locator('.page-title')).toContainText('欢迎回来');
|
||||||
|
await expect(page.locator('.page-subtitle')).toBeVisible();
|
||||||
|
await expect(page.locator('.quick-card')).toHaveCount(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clicking start study navigates to /study', async ({ page }) => {
|
||||||
|
await page.locator('.quick-card').first().locator('button').click();
|
||||||
|
await expect(page).toHaveURL(/\/study/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clicking start review navigates to /review', async ({ page }) => {
|
||||||
|
await page.locator('.quick-card').nth(1).locator('button').click();
|
||||||
|
await expect(page).toHaveURL(/\/review/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './e2e',
|
||||||
|
fullyParallel: false,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: 1,
|
||||||
|
reporter: 'list',
|
||||||
|
use: {
|
||||||
|
baseURL: 'http://localhost:5158',
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: {
|
||||||
|
...devices['Desktop Chrome'],
|
||||||
|
channel: 'chrome',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
webServer: {
|
||||||
|
command: 'pnpm dev',
|
||||||
|
url: 'http://localhost:5158',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
timeout: 30_000,
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user