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>
530 lines
17 KiB
TypeScript
530 lines
17 KiB
TypeScript
/**
|
||
* 响应式布局端到端测试
|
||
* 测试不同屏幕尺寸下的布局适配
|
||
*/
|
||
|
||
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 });
|
||
}
|