354 lines
12 KiB
TypeScript
354 lines
12 KiB
TypeScript
|
|
import { test, expect, Page } from '@playwright/test';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 学校端排课功能完整测试
|
|||
|
|
* 测试范围:Tab导航、列表视图、课表视图、日历视图、4步向导创建排课
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
const BASE_URL = 'http://localhost:5174';
|
|||
|
|
const LOGIN_CREDS = {
|
|||
|
|
username: 'school1',
|
|||
|
|
password: '123456'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
test.describe('学校端排课功能测试', () => {
|
|||
|
|
let page: Page;
|
|||
|
|
let authToken: string;
|
|||
|
|
|
|||
|
|
test.beforeAll(async ({ browser }) => {
|
|||
|
|
page = await browser.newPage();
|
|||
|
|
|
|||
|
|
// 登录获取 token
|
|||
|
|
const loginResponse = await page.request.post(`${BASE_URL}/api/v1/auth/login`, {
|
|||
|
|
data: {
|
|||
|
|
username: LOGIN_CREDS.username,
|
|||
|
|
password: LOGIN_CREDS.password
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const loginData = await loginResponse.json();
|
|||
|
|
expect(loginData.code).toBe(200);
|
|||
|
|
authToken = loginData.data.token;
|
|||
|
|
|
|||
|
|
// 设置 localStorage
|
|||
|
|
await page.goto(BASE_URL);
|
|||
|
|
await page.evaluate(([token, user]) => {
|
|||
|
|
localStorage.setItem('token', token);
|
|||
|
|
localStorage.setItem('user', JSON.stringify(user));
|
|||
|
|
}, [authToken, loginData.data]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test.afterAll(async () => {
|
|||
|
|
await page.close();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test.describe('Tab 导航切换', () => {
|
|||
|
|
test('应该能访问排课页面', async () => {
|
|||
|
|
await page.goto(`${BASE_URL}/school/schedule`);
|
|||
|
|
await page.waitForLoadState('networkidle');
|
|||
|
|
|
|||
|
|
// 验证页面标题
|
|||
|
|
const title = await page.textContent('.page-header h2, h1');
|
|||
|
|
expect(title).toContain('课程排期');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该显示三个Tab导航', async () => {
|
|||
|
|
await page.goto(`${BASE_URL}/school/schedule`);
|
|||
|
|
await page.waitForLoadState('networkidle');
|
|||
|
|
|
|||
|
|
// 验证Tab存在
|
|||
|
|
const tabs = await page.locator('a[role="tab"], .ant-tabs-tab').all();
|
|||
|
|
expect(tabs.length).toBeGreaterThanOrEqual(3);
|
|||
|
|
|
|||
|
|
const tabTexts = await Promise.all(tabs.map(tab => tab.textContent()));
|
|||
|
|
console.log('✅ 找到Tab:', tabTexts);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该能切换到列表视图', async () => {
|
|||
|
|
await page.goto(`${BASE_URL}/school/schedule`);
|
|||
|
|
await page.waitForLoadState('networkidle');
|
|||
|
|
|
|||
|
|
// 点击列表视图Tab
|
|||
|
|
const listTab = page.locator('a[role="tab"], .ant-tabs-tab').filter({ hasText: '列表' }).first();
|
|||
|
|
await listTab.click();
|
|||
|
|
await page.waitForTimeout(500);
|
|||
|
|
|
|||
|
|
// 验证列表视图内容
|
|||
|
|
const table = page.locator('.ant-table');
|
|||
|
|
await expect(table.first()).toBeVisible();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该能切换到课表视图', async () => {
|
|||
|
|
await page.goto(`${BASE_URL}/school/schedule`);
|
|||
|
|
await page.waitForLoadState('networkidle');
|
|||
|
|
|
|||
|
|
// 点击课表视图Tab
|
|||
|
|
const timetableTab = page.locator('a[role="tab"], .ant-tabs-tab').filter({ hasText: '课表' }).first();
|
|||
|
|
await timetableTab.click();
|
|||
|
|
await page.waitForTimeout(500);
|
|||
|
|
|
|||
|
|
// 验证课表视图内容
|
|||
|
|
const timetableHeader = page.locator('.timetable-header, .day-header');
|
|||
|
|
await expect(timetableHeader.first()).toBeVisible();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该能切换到日历视图', async () => {
|
|||
|
|
await page.goto(`${BASE_URL}/school/schedule`);
|
|||
|
|
await page.waitForLoadState('networkidle');
|
|||
|
|
|
|||
|
|
// 点击日历视图Tab
|
|||
|
|
const calendarTab = page.locator('a[role="tab"], .ant-tabs-tab').filter({ hasText: '日历' }).first();
|
|||
|
|
await calendarTab.click();
|
|||
|
|
await page.waitForTimeout(500);
|
|||
|
|
|
|||
|
|
// 验证日历视图内容
|
|||
|
|
const calendarView = page.locator('.ant-picker-calendar');
|
|||
|
|
await expect(calendarView.first()).toBeVisible();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test.describe('列表视图功能', () => {
|
|||
|
|
test('应该显示排课列表数据', async () => {
|
|||
|
|
await page.goto(`${BASE_URL}/school/schedule`);
|
|||
|
|
await page.waitForLoadState('networkidle');
|
|||
|
|
|
|||
|
|
// 确保在列表视图
|
|||
|
|
const listTab = page.locator('a[role="tab"], .ant-tabs-tab').filter({ hasText: '列表' }).first();
|
|||
|
|
await listTab.click();
|
|||
|
|
await page.waitForTimeout(1000);
|
|||
|
|
|
|||
|
|
// 检查表格是否存在
|
|||
|
|
const table = page.locator('.ant-table');
|
|||
|
|
await expect(table.first()).toBeVisible();
|
|||
|
|
|
|||
|
|
// 尝试获取排课数据
|
|||
|
|
const rows = await table.locator('.ant-table-tbody tr').all();
|
|||
|
|
console.log(`✅ 列表视图找到 ${rows.length} 行数据`);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该有筛选功能', async () => {
|
|||
|
|
await page.goto(`${BASE_URL}/school/schedule`);
|
|||
|
|
await page.waitForLoadState('networkidle');
|
|||
|
|
|
|||
|
|
// 检查筛选器
|
|||
|
|
const classSelect = page.locator('select[placeholder*="班级"], .ant-select').first();
|
|||
|
|
const teacherSelect = page.locator('.filter-section .ant-select').nth(1);
|
|||
|
|
|
|||
|
|
// 筛选器可能存在,记录日志
|
|||
|
|
const hasClassFilter = await classSelect.count() > 0;
|
|||
|
|
const hasTeacherFilter = await teacherSelect.count() > 0;
|
|||
|
|
|
|||
|
|
console.log(`✅ 班级筛选: ${hasClassFilter ? '存在' : '不存在'}`);
|
|||
|
|
console.log(`✅ 教师筛选: ${hasTeacherFilter ? '存在' : '不存在'}`);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test.describe('课表视图功能', () => {
|
|||
|
|
test('应该显示周次导航', async () => {
|
|||
|
|
await page.goto(`${BASE_URL}/school/schedule`);
|
|||
|
|
await page.waitForLoadState('networkidle');
|
|||
|
|
|
|||
|
|
// 切换到课表视图
|
|||
|
|
const timetableTab = page.locator('a[role="tab"], .ant-tabs-tab').filter({ hasText: '课表' }).first();
|
|||
|
|
await timetableTab.click();
|
|||
|
|
await page.waitForTimeout(1000);
|
|||
|
|
|
|||
|
|
// 检查周次导航按钮
|
|||
|
|
const prevWeekBtn = page.locator('button:has-text("上一周")');
|
|||
|
|
const nextWeekBtn = page.locator('button:has-text("下一周")');
|
|||
|
|
const currentWeekBtn = page.locator('button:has-text("本周")');
|
|||
|
|
|
|||
|
|
const hasPrevBtn = await prevWeekBtn.count() > 0;
|
|||
|
|
const hasNextBtn = await nextWeekBtn.count() > 0;
|
|||
|
|
const hasCurrentBtn = await currentWeekBtn.count() > 0;
|
|||
|
|
|
|||
|
|
console.log(`✅ 上一周按钮: ${hasPrevBtn ? '存在' : '不存在'}`);
|
|||
|
|
console.log(`✅ 下一周按钮: ${hasNextBtn ? '存在' : '不存在'}`);
|
|||
|
|
console.log(`✅ 本周按钮: ${hasCurrentBtn ? '存在' : '不存在'}`);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该显示周一到周日的列', async () => {
|
|||
|
|
await page.goto(`${BASE_URL}/school/schedule`);
|
|||
|
|
await page.waitForLoadState('networkidle');
|
|||
|
|
|
|||
|
|
const timetableTab = page.locator('a[role="tab"], .ant-tabs-tab').filter({ hasText: '课表' }).first();
|
|||
|
|
await timetableTab.click();
|
|||
|
|
await page.waitForTimeout(1000);
|
|||
|
|
|
|||
|
|
// 检查周次显示
|
|||
|
|
const dayHeaders = page.locator('.day-header, .timetable-header > div');
|
|||
|
|
const count = await dayHeaders.count();
|
|||
|
|
|
|||
|
|
console.log(`✅ 找到 ${count} 个日期列`);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test.describe('新建排课功能', () => {
|
|||
|
|
test('应该能打开新建排课弹窗', async () => {
|
|||
|
|
await page.goto(`${BASE_URL}/school/schedule`);
|
|||
|
|
await page.waitForLoadState('networkidle');
|
|||
|
|
|
|||
|
|
// 点击新建排课按钮
|
|||
|
|
const createBtn = page.locator('button:has-text("新建排课")');
|
|||
|
|
const btnCount = await createBtn.count();
|
|||
|
|
|
|||
|
|
if (btnCount > 0) {
|
|||
|
|
await createBtn.first().click();
|
|||
|
|
await page.waitForTimeout(1000);
|
|||
|
|
|
|||
|
|
// 检查弹窗是否打开
|
|||
|
|
const modal = page.locator('.ant-modal').filter({ hasText: /新建|创建|排课/ });
|
|||
|
|
const isVisible = await modal.isVisible();
|
|||
|
|
console.log(`✅ 新建排课弹窗: ${isVisible ? '已打开' : '未打开'}`);
|
|||
|
|
} else {
|
|||
|
|
console.log('⚠️ 未找到新建排课按钮');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('步骤1:应该能选择课程套餐', async () => {
|
|||
|
|
await page.goto(`${BASE_URL}/school/schedule`);
|
|||
|
|
await page.waitForLoadState('networkidle');
|
|||
|
|
|
|||
|
|
// 打开新建排课弹窗
|
|||
|
|
const createBtn = page.locator('button:has-text("新建排课")');
|
|||
|
|
if (await createBtn.count() > 0) {
|
|||
|
|
await createBtn.first().click();
|
|||
|
|
await page.waitForTimeout(1000);
|
|||
|
|
|
|||
|
|
// 检查课程套餐下拉框
|
|||
|
|
const collectionSelect = page.locator('.ant-modal .ant-select').first();
|
|||
|
|
const hasSelect = await collectionSelect.count() > 0;
|
|||
|
|
console.log(`✅ 课程套餐选择器: ${hasSelect ? '存在' : '不存在'}`);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('步骤3:应该支持为每个班级分配教师', async () => {
|
|||
|
|
await page.goto(`${BASE_URL}/school/schedule`);
|
|||
|
|
await page.waitForLoadState('networkidle');
|
|||
|
|
|
|||
|
|
// 打开新建排课弹窗
|
|||
|
|
const createBtn = page.locator('button:has-text("新建排课")');
|
|||
|
|
if (await createBtn.count() > 0) {
|
|||
|
|
await createBtn.first().click();
|
|||
|
|
await page.waitForTimeout(1000);
|
|||
|
|
|
|||
|
|
// 尝试找到教师分配相关元素
|
|||
|
|
const teacherLabel = page.locator('.ant-modal label:has-text("教师")');
|
|||
|
|
const count = await teacherLabel.count();
|
|||
|
|
console.log(`✅ 教师分配相关元素: 找到 ${count} 个`);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test.describe('API 后端测试', () => {
|
|||
|
|
test('应该能获取课程套餐列表', async () => {
|
|||
|
|
const response = await page.request.get(`${BASE_URL}/api/v1/school/packages`, {
|
|||
|
|
headers: {
|
|||
|
|
'Authorization': `Bearer ${authToken}`
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
expect(response.status()).toBe(200);
|
|||
|
|
|
|||
|
|
const data = await response.json();
|
|||
|
|
expect(data.code).toBe(200);
|
|||
|
|
|
|||
|
|
console.log(`✅ 获取课程套餐列表成功:`, data.data ? JSON.stringify(data.data).substring(0, 100) + '...' : '无数据');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该能获取课表数据', async () => {
|
|||
|
|
const today = new Date().toISOString().split('T')[0];
|
|||
|
|
const startDate = today;
|
|||
|
|
const endDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|||
|
|
|
|||
|
|
const response = await page.request.get(`${BASE_URL}/api/v1/school/schedules/timetable?startDate=${startDate}&endDate=${endDate}`, {
|
|||
|
|
headers: {
|
|||
|
|
'Authorization': `Bearer ${authToken}`
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
expect(response.status()).toBe(200);
|
|||
|
|
|
|||
|
|
const data = await response.json();
|
|||
|
|
expect(data.code).toBe(200);
|
|||
|
|
|
|||
|
|
console.log(`✅ 获取课表数据成功:`, data.data ? '有数据' : '无数据');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该能获取排课列表', async () => {
|
|||
|
|
const response = await page.request.get(`${BASE_URL}/api/v1/school/schedules?pageNum=1&pageSize=10`, {
|
|||
|
|
headers: {
|
|||
|
|
'Authorization': `Bearer ${authToken}`
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
expect(response.status()).toBe(200);
|
|||
|
|
|
|||
|
|
const data = await response.json();
|
|||
|
|
expect(data.code).toBe(200);
|
|||
|
|
|
|||
|
|
if (data.data && data.data.list) {
|
|||
|
|
console.log(`✅ 获取排课列表成功: ${data.data.list.length} 条记录`);
|
|||
|
|
} else {
|
|||
|
|
console.log(`✅ 获取排课列表成功: 无数据`);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该能获取班级列表', async () => {
|
|||
|
|
const response = await page.request.get(`${BASE_URL}/api/v1/school/classes`, {
|
|||
|
|
headers: {
|
|||
|
|
'Authorization': `Bearer ${authToken}`
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
expect(response.status()).toBe(200);
|
|||
|
|
|
|||
|
|
const data = await response.json();
|
|||
|
|
expect(data.code).toBe(200);
|
|||
|
|
|
|||
|
|
console.log(`✅ 获取班级列表成功:`, Array.isArray(data.data) ? `${data.data.length} 个班级` : '数据格式正确');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该能获取教师列表', async () => {
|
|||
|
|
const response = await page.request.get(`${BASE_URL}/api/v1/school/teachers?pageNum=1&pageSize=50`, {
|
|||
|
|
headers: {
|
|||
|
|
'Authorization': `Bearer ${authToken}`
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
expect(response.status()).toBe(200);
|
|||
|
|
|
|||
|
|
const data = await response.json();
|
|||
|
|
expect(data.code).toBe(200);
|
|||
|
|
|
|||
|
|
if (data.data && data.data.list) {
|
|||
|
|
console.log(`✅ 获取教师列表成功: ${data.data.list.length} 位教师`);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test.describe('创建排课时序测试(E2E)', () => {
|
|||
|
|
test('完整流程测试:从登录到创建排课', async () => {
|
|||
|
|
// 已登录,直接跳转到排课页面
|
|||
|
|
await page.goto(`${BASE_URL}/school/schedule`);
|
|||
|
|
await page.waitForLoadState('networkidle');
|
|||
|
|
|
|||
|
|
console.log('📍 当前页面:', page.url());
|
|||
|
|
console.log('✅ 成功访问排课页面');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('截图:当前排课页面状态', async () => {
|
|||
|
|
await page.goto(`${BASE_URL}/school/schedule`);
|
|||
|
|
await page.waitForLoadState('networkidle');
|
|||
|
|
await page.waitForTimeout(2000);
|
|||
|
|
|
|||
|
|
// 截图
|
|||
|
|
await page.screenshot({
|
|||
|
|
path: 'screenshots/schedule-page-current.png',
|
|||
|
|
fullPage: true
|
|||
|
|
});
|
|||
|
|
console.log('✅ 已保存截图: screenshots/schedule-page-current.png');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
});
|