Files
你的用户名 237c7802e5
Some checks failed
Deploy / deploy (push) Has been cancelled
Initial commit: Telegram Management System
Full-stack web application for Telegram management
- Frontend: Vue 3 + Vben Admin
- Backend: NestJS
- Features: User management, group broadcast, statistics

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 15:37:50 +08:00

530 lines
17 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 响应式布局端到端测试
* 测试不同屏幕尺寸下的布局适配
*/
import { test, expect, Page } from '@playwright/test';
const TEST_CONFIG = {
baseURL: 'http://localhost:5173',
timeout: 30000,
adminUser: {
username: 'admin',
password: '111111',
},
// 不同设备视口尺寸
viewports: {
mobile: { width: 375, height: 667 }, // iPhone SE
tablet: { width: 768, height: 1024 }, // iPad
desktop: { width: 1280, height: 720 }, // 桌面
largeDesktop: { width: 1920, height: 1080 }, // 大屏桌面
},
};
test.describe('桌面端响应式测试', () => {
test.beforeEach(async ({ page }) => {
test.setTimeout(TEST_CONFIG.timeout);
// 设置桌面端视口
await page.setViewportSize(TEST_CONFIG.viewports.desktop);
await page.goto(TEST_CONFIG.baseURL);
await loginAsAdmin(page);
});
test('桌面端布局应该正确显示', async ({ page }) => {
// 检查侧边栏是否完全展开
const sidebar = page.locator('.layout-sidebar');
await expect(sidebar).toBeVisible();
await expect(sidebar).toHaveClass(/expanded|full/);
// 检查侧边栏菜单项是否显示文字
const menuItem = page.locator('.menu-item').first();
await expect(menuItem.locator('.menu-title')).toBeVisible();
// 检查主内容区域
const mainContent = page.locator('.layout-content');
await expect(mainContent).toBeVisible();
// 检查头部导航
const header = page.locator('.layout-header');
await expect(header).toBeVisible();
await expect(header.locator('.breadcrumb')).toBeVisible();
await expect(header.locator('.user-dropdown')).toBeVisible();
});
test('侧边栏折叠展开功能应该正常工作', async ({ page }) => {
const sidebar = page.locator('.layout-sidebar');
const toggleButton = page.locator('[data-testid="sidebar-toggle"]');
// 点击折叠按钮
await toggleButton.click();
// 等待动画完成
await page.waitForTimeout(500);
// 检查侧边栏是否折叠
await expect(sidebar).toHaveClass(/collapsed|mini/);
// 检查菜单项文字是否隐藏
const menuTitle = page.locator('.menu-item .menu-title').first();
await expect(menuTitle).not.toBeVisible();
// 再次点击展开
await toggleButton.click();
await page.waitForTimeout(500);
// 检查侧边栏是否展开
await expect(sidebar).toHaveClass(/expanded|full/);
await expect(menuTitle).toBeVisible();
});
test('桌面端表格应该显示所有列', async ({ page }) => {
// 导航到账号列表页面
await page.click('[data-testid="menu-account"]');
await page.click('[data-testid="menu-account-list"]');
await page.waitForURL(/\/account\/list/);
// 检查表格列数
const tableHeaders = page.locator('.ant-table-thead th');
const headerCount = await tableHeaders.count();
// 桌面端应该显示所有列至少6列
expect(headerCount).toBeGreaterThanOrEqual(6);
// 检查操作列是否显示完整
const actionColumn = page
.locator('.ant-table-thead th')
.filter({ hasText: /操作|Actions/ });
await expect(actionColumn).toBeVisible();
// 检查操作按钮是否都可见
const firstRow = page.locator('.ant-table-tbody tr').first();
await expect(firstRow.locator('[data-testid="view-button"]')).toBeVisible();
await expect(firstRow.locator('[data-testid="edit-button"]')).toBeVisible();
await expect(
firstRow.locator('[data-testid="delete-button"]'),
).toBeVisible();
});
});
test.describe('平板端响应式测试', () => {
test.beforeEach(async ({ page }) => {
// 设置平板端视口
await page.setViewportSize(TEST_CONFIG.viewports.tablet);
await page.goto(TEST_CONFIG.baseURL);
await loginAsAdmin(page);
});
test('平板端布局应该适配正确', async ({ page }) => {
// 检查侧边栏状态
const sidebar = page.locator('.layout-sidebar');
await expect(sidebar).toBeVisible();
// 平板端侧边栏可能默认折叠
const isCollapsed = await sidebar.evaluate(
(el) =>
el.classList.contains('collapsed') || el.classList.contains('mini'),
);
if (isCollapsed) {
// 检查折叠状态下的菜单
const menuIcons = page.locator('.menu-item .menu-icon');
await expect(menuIcons.first()).toBeVisible();
}
// 检查主内容区域是否合理布局
const mainContent = page.locator('.layout-content');
await expect(mainContent).toBeVisible();
// 检查头部导航是否适配
const header = page.locator('.layout-header');
await expect(header).toBeVisible();
});
test('平板端表格应该隐藏次要列', async ({ page }) => {
// 导航到账号列表页面
await page.click('[data-testid="menu-account"]');
await page.click('[data-testid="menu-account-list"]');
await page.waitForURL(/\/account\/list/);
// 检查表格是否使用响应式设计
const table = page.locator('.ant-table');
await expect(table).toBeVisible();
// 平板端可能隐藏一些次要列
const visibleHeaders = page.locator('.ant-table-thead th:visible');
const visibleHeaderCount = await visibleHeaders.count();
// 平板端显示的列数应该少于桌面端
expect(visibleHeaderCount).toBeLessThan(8);
expect(visibleHeaderCount).toBeGreaterThan(3);
});
test('平板端搜索表单应该优化布局', async ({ page }) => {
// 导航到有搜索表单的页面
await page.click('[data-testid="menu-account"]');
await page.click('[data-testid="menu-account-list"]');
await page.waitForURL(/\/account\/list/);
// 检查搜索表单布局
const searchForm = page.locator('.search-form');
await expect(searchForm).toBeVisible();
// 平板端搜索表单可能采用不同的布局
const formItems = page.locator('.ant-form-item');
const itemCount = await formItems.count();
// 检查表单项是否合理排列
expect(itemCount).toBeGreaterThan(0);
});
});
test.describe('手机端响应式测试', () => {
test.beforeEach(async ({ page }) => {
// 设置手机端视口
await page.setViewportSize(TEST_CONFIG.viewports.mobile);
await page.goto(TEST_CONFIG.baseURL);
await loginAsAdmin(page);
});
test('手机端应该显示移动端导航', async ({ page }) => {
// 检查移动端汉堡菜单按钮
const hamburgerButton = page.locator('[data-testid="mobile-menu-toggle"]');
await expect(hamburgerButton).toBeVisible();
// 检查侧边栏是否隐藏
const sidebar = page.locator('.layout-sidebar');
const isHidden = await sidebar.evaluate(
(el) =>
getComputedStyle(el).display === 'none' ||
el.classList.contains('mobile-hidden'),
);
expect(isHidden).toBe(true);
});
test('手机端侧边栏抽屉应该正常工作', async ({ page }) => {
// 点击汉堡菜单按钮
const hamburgerButton = page.locator('[data-testid="mobile-menu-toggle"]');
await hamburgerButton.click();
// 等待抽屉动画
await page.waitForTimeout(500);
// 检查抽屉是否打开
const drawer = page.locator('.ant-drawer, .mobile-sidebar-drawer');
await expect(drawer).toBeVisible();
// 检查菜单项是否显示
const menuItems = drawer.locator('.menu-item');
await expect(menuItems.first()).toBeVisible();
// 点击遮罩层关闭抽屉
const mask = page.locator('.ant-drawer-mask');
if ((await mask.count()) > 0) {
await mask.click();
await page.waitForTimeout(500);
await expect(drawer).not.toBeVisible();
}
});
test('手机端表格应该使用卡片布局', async ({ page }) => {
// 导航到账号列表页面
await page.click('[data-testid="mobile-menu-toggle"]');
await page.waitForTimeout(300);
await page.click('[data-testid="menu-account"]');
await page.click('[data-testid="menu-account-list"]');
await page.waitForURL(/\/account\/list/);
// 手机端可能使用卡片列表而不是表格
const cardList = page.locator('.mobile-card-list, .account-cards');
const table = page.locator('.ant-table');
// 检查是否使用了移动端适配的布局
if ((await cardList.count()) > 0) {
await expect(cardList).toBeVisible();
// 检查卡片项
const cardItems = cardList.locator('.card-item');
if ((await cardItems.count()) > 0) {
await expect(cardItems.first()).toBeVisible();
}
} else {
// 如果仍使用表格,检查是否为响应式表格
await expect(table).toBeVisible();
// 检查表格是否可以横向滚动
const tableWrapper = page.locator('.ant-table-wrapper');
const hasScroll = await tableWrapper.evaluate(
(el) => el.scrollWidth > el.clientWidth,
);
if (hasScroll) {
// 表格应该可以滚动
expect(hasScroll).toBe(true);
}
}
});
test('手机端搜索应该使用折叠形式', async ({ page }) => {
// 导航到有搜索功能的页面
await page.click('[data-testid="mobile-menu-toggle"]');
await page.waitForTimeout(300);
await page.click('[data-testid="menu-account"]');
await page.click('[data-testid="menu-account-list"]');
await page.waitForURL(/\/account\/list/);
// 检查搜索按钮或搜索图标
const searchTrigger = page.locator(
'[data-testid="search-trigger"], [data-testid="mobile-search-button"]',
);
if ((await searchTrigger.count()) > 0) {
await expect(searchTrigger).toBeVisible();
// 点击展开搜索
await searchTrigger.click();
await page.waitForTimeout(300);
// 检查搜索表单是否展开
const searchForm = page.locator('.search-form, .mobile-search-form');
await expect(searchForm).toBeVisible();
}
});
test('手机端表单应该全宽显示', async ({ page }) => {
// 导航到表单页面(如创建账号)
await page.click('[data-testid="mobile-menu-toggle"]');
await page.waitForTimeout(300);
await page.click('[data-testid="menu-account"]');
await page.click('[data-testid="menu-account-list"]');
await page.waitForURL(/\/account\/list/);
// 点击添加按钮
const addButton = page.locator('[data-testid="add-account-button"]');
await addButton.click();
// 等待弹窗或新页面打开
const modal = page.locator('.ant-modal');
const formPage = page.locator('.form-page');
if ((await modal.count()) > 0) {
// 如果是弹窗,检查弹窗是否适配移动端
await expect(modal).toBeVisible();
// 检查弹窗宽度是否适配
const modalWidth = await modal.evaluate(
(el) => getComputedStyle(el).width,
);
expect(modalWidth).toMatch(/100%|90%|95%/);
} else if ((await formPage.count()) > 0) {
// 如果是新页面,检查表单布局
await expect(formPage).toBeVisible();
}
});
});
test.describe('大屏幕响应式测试', () => {
test.beforeEach(async ({ page }) => {
// 设置大屏幕视口
await page.setViewportSize(TEST_CONFIG.viewports.largeDesktop);
await page.goto(TEST_CONFIG.baseURL);
await loginAsAdmin(page);
});
test('大屏幕应该显示更多内容', async ({ page }) => {
// 检查布局是否充分利用屏幕空间
const mainContent = page.locator('.layout-content');
await expect(mainContent).toBeVisible();
// 检查内容区域宽度
const contentWidth = await mainContent.evaluate((el) => el.offsetWidth);
expect(contentWidth).toBeGreaterThan(1000);
// 检查是否有合理的最大宽度限制
expect(contentWidth).toBeLessThan(1800);
});
test('大屏幕表格应该显示所有列', async ({ page }) => {
// 导航到表格页面
await page.click('[data-testid="menu-account"]');
await page.click('[data-testid="menu-account-list"]');
await page.waitForURL(/\/account\/list/);
// 检查表格列数
const tableHeaders = page.locator('.ant-table-thead th');
const headerCount = await tableHeaders.count();
// 大屏幕应该显示最多的列
expect(headerCount).toBeGreaterThanOrEqual(8);
// 检查表格是否有合理的宽度
const table = page.locator('.ant-table');
const tableWidth = await table.evaluate((el) => el.offsetWidth);
expect(tableWidth).toBeGreaterThan(800);
});
test('大屏幕仪表盘应该显示更多组件', async ({ page }) => {
// 导航到仪表盘
await page.click('[data-testid="menu-dashboard"]');
await page.waitForURL(/\/dashboard/);
// 检查仪表盘组件
const dashboardCards = page.locator('.dashboard-card, .statistics-card');
const cardCount = await dashboardCards.count();
// 大屏幕应该显示更多卡片
expect(cardCount).toBeGreaterThan(4);
// 检查图表容器
const chartContainers = page.locator(
'.chart-container, .echarts-container',
);
const chartCount = await chartContainers.count();
if (chartCount > 0) {
// 检查图表是否充分利用空间
const firstChart = chartContainers.first();
const chartWidth = await firstChart.evaluate((el) => el.offsetWidth);
expect(chartWidth).toBeGreaterThan(400);
}
});
});
test.describe('响应式断点测试', () => {
const breakpoints = [
{ name: '超小屏', width: 320 },
{ name: '小屏', width: 576 },
{ name: '中屏', width: 768 },
{ name: '大屏', width: 992 },
{ name: '超大屏', width: 1200 },
{ name: '巨屏', width: 1600 },
];
breakpoints.forEach(({ name, width }) => {
test(`${name}(${width}px)断点应该正确适配`, async ({ page }) => {
// 设置视口尺寸
await page.setViewportSize({ width, height: 800 });
await page.goto(TEST_CONFIG.baseURL);
await loginAsAdmin(page);
// 检查基本布局元素
const header = page.locator('.layout-header');
const content = page.locator('.layout-content');
await expect(header).toBeVisible();
await expect(content).toBeVisible();
// 检查布局是否适配当前尺寸
const contentWidth = await content.evaluate((el) => el.offsetWidth);
expect(contentWidth).toBeLessThanOrEqual(width);
expect(contentWidth).toBeGreaterThan(width * 0.8); // 至少使用80%的宽度
// 导航到列表页面测试表格适配
if (width > 576) {
// 大于576px时才测试表格
await page.click('[data-testid="menu-account"]');
await page.click('[data-testid="menu-account-list"]');
await page.waitForURL(/\/account\/list/);
const table = page.locator('.ant-table, .mobile-card-list');
await expect(table).toBeVisible();
}
});
});
});
test.describe('触摸设备交互测试', () => {
test.beforeEach(async ({ page }) => {
// 模拟触摸设备
await page.setViewportSize(TEST_CONFIG.viewports.mobile);
await page.goto(TEST_CONFIG.baseURL);
await loginAsAdmin(page);
});
test('触摸滑动应该正常工作', async ({ page }) => {
// 导航到有滚动内容的页面
await page.click('[data-testid="mobile-menu-toggle"]');
await page.waitForTimeout(300);
await page.click('[data-testid="menu-account"]');
await page.click('[data-testid="menu-account-list"]');
await page.waitForURL(/\/account\/list/);
// 检查是否可以滚动
const scrollContainer = page.locator(
'.ant-table-wrapper, .mobile-card-list',
);
if ((await scrollContainer.count()) > 0) {
// 模拟触摸滚动
await scrollContainer.hover();
await page.mouse.wheel(0, 100);
// 等待滚动完成
await page.waitForTimeout(500);
// 检查滚动是否生效
const scrollTop = await scrollContainer.evaluate((el) => el.scrollTop);
expect(scrollTop).toBeGreaterThanOrEqual(0);
}
});
test('触摸点击区域应该足够大', async ({ page }) => {
// 检查按钮点击区域
const buttons = page.locator('button, .ant-btn');
if ((await buttons.count()) > 0) {
const firstButton = buttons.first();
// 检查按钮尺寸触摸友好的最小尺寸是44x44px
const buttonBox = await firstButton.boundingBox();
if (buttonBox) {
expect(buttonBox.height).toBeGreaterThanOrEqual(40);
expect(buttonBox.width).toBeGreaterThanOrEqual(40);
}
}
});
test('长按菜单应该正常工作', async ({ page }) => {
// 导航到有右键菜单的区域
await page.click('[data-testid="mobile-menu-toggle"]');
await page.waitForTimeout(300);
await page.click('[data-testid="menu-account"]');
await page.click('[data-testid="menu-account-list"]');
await page.waitForURL(/\/account\/list/);
// 查找表格行或卡片项
const listItem = page.locator('.ant-table-tbody tr, .card-item').first();
if ((await listItem.count()) > 0) {
// 模拟长按
await listItem.hover();
await page.mouse.down();
await page.waitForTimeout(1000); // 长按1秒
await page.mouse.up();
// 检查是否显示上下文菜单
const contextMenu = page.locator('.ant-dropdown, .context-menu');
if ((await contextMenu.count()) > 0) {
await expect(contextMenu).toBeVisible();
}
}
});
});
// 辅助函数:管理员登录
async function loginAsAdmin(page: Page) {
await page.fill(
'[data-testid="username-input"]',
TEST_CONFIG.adminUser.username,
);
await page.fill(
'[data-testid="password-input"]',
TEST_CONFIG.adminUser.password,
);
await page.click('[data-testid="login-button"]');
await page.waitForURL(/\/dashboard/, { timeout: 10000 });
}