Initial commit: Telegram Management System
Some checks failed
Deploy / deploy (push) Has been cancelled

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>
This commit is contained in:
你的用户名
2025-11-04 15:37:50 +08:00
commit 237c7802e5
3674 changed files with 525172 additions and 0 deletions

View File

@@ -0,0 +1,529 @@
/**
* 响应式布局端到端测试
* 测试不同屏幕尺寸下的布局适配
*/
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 });
}