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