healthapp
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

837 lines
32 KiB

/**
* 商城前端(mall)自动化测试脚本
* 认证策略:公开浏览(首页/分类/商品/搜索)→ 需登录操作(购物车/订单/支付)
* 测试流程:
* Phase A: 未登录公开浏览(首页、分类、商品详情、搜索)
* Phase B: 未登录访问受保护页面(重定向到登录页)
* Phase C: 登录后完整功能(购物车、订单、地址、会员)
*
* 运行方式:
* cd tests
* node mall.test.js
*
* 前置条件:
* 1. mall 项目运行中:cd mall && npm run dev (http://localhost:5174)
* 2. 后端 API 运行中(可选,部分测试不依赖后端数据)
*/
const { chromium } = require('playwright');
const MALL_URL = 'http://localhost:5174';
const HEALTH_AI_URL = 'http://localhost:5173';
// 模拟 token 和用户信息(与健康助手共享的 localStorage 数据)
const MOCK_TOKEN = 'test-token-for-mall-e2e';
const MOCK_USER = JSON.stringify({
id: 1,
phone: '13800138000',
nickname: '测试用户',
avatar: '',
surveyCompleted: true
});
// 测试结果统计
const testResults = {
passed: 0,
failed: 0,
tests: []
};
function logTest(name, passed, detail = '') {
const status = passed ? '✓' : '✗';
const msg = `${status} ${name}${detail ? ': ' + detail : ''}`;
console.log(msg);
testResults.tests.push({ name, passed, detail });
if (passed) testResults.passed++;
else testResults.failed++;
}
/**
* 设置登录态(通过 addInitScript 在页面加载前注入 localStorage)
*/
async function setupAuth(context, page) {
console.log('\n【步骤0】设置登录态 (localStorage)...');
// 使用 addInitScript 在页面脚本执行前注入 token,避免路由守卫重定向
await context.addInitScript(({ token, user }) => {
try {
localStorage.setItem('token', token);
localStorage.setItem('user', user);
} catch (e) {
// about:blank 等页面无法访问 localStorage
}
}, { token: MOCK_TOKEN, user: MOCK_USER });
logTest('设置登录态', true, '已通过 addInitScript 注入 token 和 user');
}
/**
* Mock 后端 API 响应(避免 401 重定向和网络错误)
*/
async function setupApiMocks(page) {
console.log(' 设置 API Mock...');
// 拦截所有 /api/mall/* 请求,返回合理的 mock 数据
await page.route('**/api/mall/cart', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ items: [], total_count: 0, selected_count: 0, total_amount: 0 })
});
});
await page.route('**/api/mall/categories', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
categories: [
{ id: 1, name: '保健滋补', parent_id: 0, icon: '🫚', description: '', sort: 1 },
{ id: 2, name: '养生茶饮', parent_id: 0, icon: '🍵', description: '', sort: 2 },
{ id: 3, name: '草本膳食', parent_id: 0, icon: '🌿', description: '', sort: 3 },
{ id: 4, name: '运动营养', parent_id: 0, icon: '💪', description: '', sort: 4 },
{ id: 5, name: '母婴健康', parent_id: 0, icon: '👶', description: '', sort: 5 },
{ id: 6, name: '个护清洁', parent_id: 0, icon: '🧴', description: '', sort: 6 }
]
})
});
});
await page.route('**/api/mall/products/featured*', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
products: [
{ id: 1, name: '枸杞红枣养生茶', description: '天然滋补好茶', main_image: '', price: 49.90, original_price: 68.00, sales_count: 235, stock: 100, is_featured: true, constitution_types: ['qixu'], health_tags: ['补气养血'] },
{ id: 2, name: '黄芪片', description: '精选优质黄芪', main_image: '', price: 39.90, original_price: 52.00, sales_count: 128, stock: 50, is_featured: true, constitution_types: ['yangxu'], health_tags: ['补气固表'] }
]
})
});
});
await page.route('**/api/mall/products/search*', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ products: [], total: 0, page: 1, page_size: 20 })
});
});
await page.route('**/api/mall/products/constitution-recommend*', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ products: [] })
});
});
await page.route('**/api/mall/products/*', (route) => {
if (route.request().method() === 'GET') {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
id: 1, category_id: 1, name: '枸杞红枣养生茶', description: '精选优质枸杞红枣',
main_image: '', images: [], price: 49.90, original_price: 68.00, stock: 100,
sales_count: 235, is_featured: true, constitution_types: ['qixu'],
health_tags: ['补气养血', '滋阴润燥'], efficacy: '补气养血', ingredients: '枸杞、红枣',
usage: '每日一杯', contraindications: '孕妇慎用', skus: []
})
});
} else {
route.continue();
}
});
await page.route('**/api/mall/products', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ products: [], total: 0, page: 1, page_size: 20 })
});
});
await page.route('**/api/mall/addresses*', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([])
});
});
await page.route('**/api/mall/orders/preview', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
items: [{ id: 1, product_id: 1, sku_id: 0, product_name: '枸杞红枣养生茶', sku_name: '', image: '', price: 49.90, quantity: 1, selected: true, stock: 100 }],
total_amount: 49.90, discount_amount: 0, shipping_fee: 0, pay_amount: 49.90, max_points_use: 0, points_discount: 0
})
});
});
await page.route('**/api/mall/orders*', (route) => {
const url = new URL(route.request().url());
const status = url.searchParams.get('status');
const statusTotalMap = {
pending: 3,
paid: 2,
shipped: 1,
completed: 5
};
const total = status ? (statusTotalMap[status] || 0) : 0;
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
orders: [],
page_info: { total, page: 1, page_size: 20 },
total,
page: 1,
page_size: 20
})
});
});
await page.route('**/api/mall/member/info', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
level: 'normal', level_name: '普通会员', total_spent: 0, points: 100,
member_since: '2026-01-01', next_level: 'silver', next_level_spent: 500,
discount: 1.0, points_multiplier: 1, free_shipping_min: 99
})
});
});
await page.route('**/api/mall/member/points/records*', (route) => {
const url = new URL(route.request().url());
const pageNo = Number(url.searchParams.get('page') || '1');
const pageSize = Number(url.searchParams.get('page_size') || '10');
const all = Array.from({ length: 12 }).map((_, idx) => ({
id: idx + 1,
type: idx % 2 === 0 ? 'earn' : 'order',
points: idx % 2 === 0 ? 10 : -5,
balance: 100 + idx,
source: 'test',
remark: `积分记录${idx + 1}`,
created_at: '2026-02-01 10:00:00'
}));
const start = (pageNo - 1) * pageSize;
const records = all.slice(start, start + pageSize);
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
records,
page_info: { total: all.length, page: pageNo, page_size: pageSize },
total: all.length,
page: pageNo,
page_size: pageSize
})
});
});
logTest('API Mock 设置', true);
}
/**
* 测试 1:商城首页
*/
async function testHomePage(page) {
console.log('\n【步骤1】验证商城首页...');
await page.goto(MALL_URL, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
await page.waitForTimeout(2000);
// 截图
await page.screenshot({ path: 'screenshots/mall-home.png', fullPage: true }).catch(() => {});
// 检查页面标题/header
const headerTitle = await page.locator('.header-title').first().textContent().catch(() => '');
logTest('首页标题', headerTitle?.includes('健康商城'), `标题: "${headerTitle}"`);
// 检查搜索栏
const searchBar = page.locator('.search-bar');
const searchVisible = await searchBar.isVisible().catch(() => false);
logTest('搜索栏', searchVisible);
// 检查 Banner 轮播
const bannerSection = page.locator('.banner-section');
const bannerVisible = await bannerSection.isVisible().catch(() => false);
logTest('Banner轮播', bannerVisible);
// 检查体质推荐卡片
const constitutionCard = page.locator('.constitution-card');
const constitutionVisible = await constitutionCard.isVisible().catch(() => false);
logTest('体质推荐卡片', constitutionVisible);
// 检查底部TabBar
const tabbar = page.locator('.mall-tabbar');
const tabbarVisible = await tabbar.isVisible().catch(() => false);
logTest('底部TabBar', tabbarVisible);
// 检查TabBar包含4个Tab
const tabItems = page.locator('.mall-tabbar .tab-item');
const tabCount = await tabItems.count();
logTest('TabBar项数量', tabCount === 4, `实际: ${tabCount}`);
// 检查AI咨询悬浮按钮
const aiBtn = page.locator('.ai-float-btn');
const aiBtnVisible = await aiBtn.isVisible().catch(() => false);
logTest('AI咨询悬浮按钮', aiBtnVisible);
// 检查热销推荐标题
const sectionTitle = await page.locator('.section-title').first().textContent().catch(() => '');
logTest('热销推荐区域', sectionTitle?.includes('热销'), `标题: "${sectionTitle}"`);
}
/**
* 测试 2:底部Tab导航
*/
async function testTabNavigation(page) {
console.log('\n【步骤2】测试底部Tab导航...');
// 点击 "分类" Tab
const categoryTab = page.locator('.mall-tabbar .tab-item').nth(1);
await categoryTab.click();
await page.waitForTimeout(1500);
const currentUrl1 = page.url();
logTest('导航到分类页', currentUrl1.includes('/category'), `URL: ${currentUrl1}`);
await page.screenshot({ path: 'screenshots/mall-category.png', fullPage: true }).catch(() => {});
// 检查分类页标题更新
const categoryTitle = await page.locator('.header-title').first().textContent().catch(() => '');
logTest('分类页标题', categoryTitle?.includes('分类'), `标题: "${categoryTitle}"`);
// 点击 "购物车" Tab
const cartTab = page.locator('.mall-tabbar .tab-item').nth(2);
await cartTab.click();
await page.waitForTimeout(2000);
const currentUrl2 = page.url();
logTest('导航到购物车页', currentUrl2.includes('/cart'), `URL: ${currentUrl2}`);
await page.screenshot({ path: 'screenshots/mall-cart.png', fullPage: true }).catch(() => {});
// 检查购物车页面标题
const cartTitle = await page.locator('.header-title').first().textContent({ timeout: 3000 }).catch(() => '');
logTest('购物车页标题', cartTitle?.includes('购物车'), `标题: "${cartTitle}"`);
// 购物车为空时应显示空状态(mock 返回空购物车)
const emptyState = page.locator('.cart-empty, .el-empty');
const emptyVisible = await emptyState.first().isVisible({ timeout: 5000 }).catch(() => false);
logTest('购物车空状态', emptyVisible, emptyVisible ? '显示空购物车' : '有商品或未显示');
// 点击 "我的" Tab
const memberTab = page.locator('.mall-tabbar .tab-item').nth(3);
await memberTab.click();
await page.waitForTimeout(1500);
const currentUrl3 = page.url();
logTest('导航到会员页', currentUrl3.includes('/member'), `URL: ${currentUrl3}`);
await page.screenshot({ path: 'screenshots/mall-member.png', fullPage: true }).catch(() => {});
// 回到首页
const homeTab = page.locator('.mall-tabbar .tab-item').nth(0);
await homeTab.click();
await page.waitForTimeout(1500);
const currentUrl4 = page.url();
logTest('回到首页', currentUrl4.endsWith('/') || currentUrl4 === MALL_URL || currentUrl4 === MALL_URL + '/', `URL: ${currentUrl4}`);
}
/**
* 测试 3:搜索页导航
*/
async function testSearchNavigation(page) {
console.log('\n【步骤3】测试搜索页导航...');
// 点击搜索栏
const searchBar = page.locator('.search-bar');
await searchBar.click();
await page.waitForTimeout(1500);
const currentUrl = page.url();
logTest('搜索栏跳转', currentUrl.includes('/search'), `URL: ${currentUrl}`);
await page.screenshot({ path: 'screenshots/mall-search.png', fullPage: true }).catch(() => {});
// 检查搜索页标题
const searchTitle = await page.locator('.header-title').first().textContent().catch(() => '');
logTest('搜索页标题', searchTitle?.includes('搜索'), `标题: "${searchTitle}"`);
// 检查有返回箭头(非 tabBar 页面)
const backBtn = page.locator('.back-btn');
const backVisible = await backBtn.isVisible().catch(() => false);
logTest('搜索页返回按钮', backVisible);
// 返回首页
await backBtn.click().catch(() => {});
await page.waitForTimeout(1000);
}
/**
* 测试 4:Header 组件
*/
async function testHeaderComponents(page) {
console.log('\n【步骤4】测试Header组件...');
await page.goto(MALL_URL, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
await page.waitForTimeout(1000);
// 检查 header 右侧搜索图标
const headerSearchIcon = page.locator('.header-right .header-icon').first();
const searchIconVisible = await headerSearchIcon.isVisible().catch(() => false);
logTest('Header搜索图标', searchIconVisible);
// 点击 header 搜索图标
await headerSearchIcon.click();
await page.waitForTimeout(1000);
const urlAfterSearch = page.url();
logTest('Header搜索图标跳转', urlAfterSearch.includes('/search'), `URL: ${urlAfterSearch}`);
// 回到首页
await page.goto(MALL_URL, { waitUntil: 'networkidle', timeout: 10000 }).catch(() => {});
await page.waitForTimeout(1000);
// 检查 header 购物车图标(角标为空时不一定存在 .cart-badge)
const headerCartIcon = page.locator('.header-right .header-icon').nth(1);
const cartIconVisible = await headerCartIcon.isVisible().catch(() => false);
logTest('Header购物车图标', cartIconVisible);
// 点击购物车图标应跳转到购物车
await headerCartIcon.click();
await page.waitForTimeout(1000);
const urlAfterCart = page.url();
logTest('Header购物车跳转', urlAfterCart.includes('/cart'), `URL: ${urlAfterCart}`);
}
/**
* 测试 5:收货地址页
*/
async function testAddressPage(page) {
console.log('\n【步骤5】测试收货地址页...');
await page.goto(MALL_URL + '/address', { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
await page.waitForTimeout(2000);
await page.screenshot({ path: 'screenshots/mall-address.png', fullPage: true }).catch(() => {});
// 检查页面标题
const title = await page.locator('.header-title').first().textContent().catch(() => '');
logTest('地址页标题', title?.includes('地址'), `标题: "${title}"`);
// 检查返回按钮(非tabBar页面)
const backBtn = page.locator('.back-btn');
const backVisible = await backBtn.isVisible().catch(() => false);
logTest('地址页返回按钮', backVisible);
// 检查底部添加按钮
const addBtn = page.locator('.add-btn');
const addBtnVisible = await addBtn.isVisible().catch(() => false);
logTest('新增地址按钮', addBtnVisible);
// 点击新增地址按钮,弹出抽屉
if (addBtnVisible) {
await addBtn.click();
await page.waitForTimeout(1000);
// 检查抽屉弹出
const drawer = page.locator('.el-drawer');
const drawerVisible = await drawer.isVisible({ timeout: 3000 }).catch(() => false);
logTest('新增地址抽屉弹出', drawerVisible);
await page.screenshot({ path: 'screenshots/mall-address-drawer.png', fullPage: true }).catch(() => {});
// 检查表单字段
const formItems = page.locator('.address-form .el-form-item');
const formCount = await formItems.count();
logTest('地址表单字段', formCount >= 6, `字段数: ${formCount}`);
// 关闭抽屉
const cancelBtn = page.locator('.cancel-btn');
if (await cancelBtn.isVisible().catch(() => false)) {
await cancelBtn.click();
await page.waitForTimeout(500);
}
}
}
/**
* 测试 6:订单页
*/
async function testOrdersPage(page) {
console.log('\n【步骤6】测试订单页...');
await page.goto(MALL_URL + '/orders', { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
await page.waitForTimeout(2000);
await page.screenshot({ path: 'screenshots/mall-orders.png', fullPage: true }).catch(() => {});
// 检查页面标题
const title = await page.locator('.header-title').first().textContent().catch(() => '');
logTest('订单页标题', title?.includes('订单'), `标题: "${title}"`);
// 检查返回按钮
const backBtn = page.locator('.back-btn');
const backVisible = await backBtn.isVisible().catch(() => false);
logTest('订单页返回按钮', backVisible);
}
/**
* 测试 6B:我的页面增强功能(订单角标、积分分页、退出弹窗)
*/
async function testMemberEnhancements(page) {
console.log('\n【步骤6B】测试我的页面增强功能...');
await page.goto(MALL_URL + '/member', { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
await page.waitForTimeout(1500);
// 订单状态角标(来自 /orders?status=xxx 统计)
const badgeCount = await page.locator('.order-tab-badge').count();
logTest('我的页订单状态角标', badgeCount > 0, `角标数量: ${badgeCount}`);
// 积分明细分页加载(第2页追加)
await page.getByText('积分明细').first().click().catch(() => {});
await page.waitForTimeout(1200);
const beforeCount = await page.locator('.points-record').count();
const loadMoreBtn = page.getByRole('button', { name: '加载更多' });
const canLoadMore = await loadMoreBtn.isVisible().catch(() => false);
if (canLoadMore) {
await loadMoreBtn.click();
await page.waitForTimeout(1200);
}
const afterCount = await page.locator('.points-record').count();
logTest('积分明细加载更多追加', afterCount > beforeCount, `之前: ${beforeCount}, 现在: ${afterCount}`);
// 关闭积分抽屉,避免遮罩影响后续点击
await page.keyboard.press('Escape').catch(() => {});
await page.waitForTimeout(300);
// 退出登录入口:出现确认弹窗(取消即可,不影响后续)
await page.getByText('退出登录').first().click().catch(() => {});
await page.waitForTimeout(500);
const logoutDialog = page.locator('.el-message-box');
const logoutDialogVisible = await logoutDialog.isVisible().catch(() => false);
logTest('退出登录确认弹窗', logoutDialogVisible);
if (logoutDialogVisible) {
await page.getByRole('button', { name: '取消' }).click().catch(() => {});
}
}
/**
* 测试 6C:购物车增强功能(删除已选、清空购物车)
*/
async function testCartEnhancements(browser) {
console.log('\n【步骤6C】测试购物车增强功能...');
const context = await browser.newContext({
viewport: { width: 750, height: 1334 },
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15'
});
const page = await context.newPage();
await setupAuth(context, page);
await setupApiMocks(page);
const state = {
items: [
{ id: 1, product_id: 1, sku_id: 0, product_name: '枸杞红枣养生茶', sku_name: '', image: '', price: 49.9, quantity: 1, selected: true, stock: 20 },
{ id: 2, product_id: 2, sku_id: 0, product_name: '黄芪片', sku_name: '', image: '', price: 39.9, quantity: 2, selected: false, stock: 20 }
]
};
// 覆盖购物车相关 mock,模拟真实增删清流程
await page.route('**/api/mall/cart/clear', async (route) => {
state.items = [];
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) });
});
await page.route('**/api/mall/cart/*', async (route) => {
const req = route.request();
const url = new URL(req.url());
if (url.pathname.endsWith('/cart/clear') && req.method() === 'DELETE') {
state.items = [];
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) });
return;
}
const id = Number(url.pathname.split('/').pop());
if (req.method() === 'DELETE') {
state.items = state.items.filter((i) => i.id !== id);
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) });
return;
}
if (req.method() === 'PUT') {
const body = req.postDataJSON();
state.items = state.items.map((i) => (i.id === id ? { ...i, ...body } : i));
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) });
return;
}
await route.continue();
});
await page.route('**/api/mall/cart', async (route) => {
const selectedItems = state.items.filter((i) => i.selected);
const totalAmount = selectedItems.reduce((sum, i) => sum + i.price * i.quantity, 0);
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
items: state.items,
total_count: state.items.length,
selected_count: selectedItems.length,
total_amount: Number(totalAmount.toFixed(2))
})
});
});
try {
await page.goto(MALL_URL + '/cart', { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
await page.waitForTimeout(1200);
const deleteSelectedBtn = page.getByRole('button', { name: '删除已选' });
const clearCartBtn = page.getByRole('button', { name: '清空购物车' });
logTest('购物车增强-删除已选按钮', await deleteSelectedBtn.isVisible().catch(() => false));
logTest('购物车增强-清空购物车按钮', await clearCartBtn.isVisible().catch(() => false));
await deleteSelectedBtn.click();
await page.locator('.el-message-box__btns .el-button--primary').first().click().catch(() => {});
await page.waitForTimeout(1000);
const itemCountAfterDelete = await page.locator('.cart-item').count();
logTest('购物车增强-删除已选生效', itemCountAfterDelete === 1, `剩余: ${itemCountAfterDelete}`);
await clearCartBtn.click();
await page.locator('.el-message-box__btns .el-button--primary').first().click().catch(() => {});
await page.waitForTimeout(1000);
const emptyVisible = await page.locator('.cart-empty').isVisible().catch(() => false);
logTest('购物车增强-清空购物车生效', emptyVisible);
} finally {
await context.close();
}
}
/**
* 测试 7A:未登录公开浏览(应能正常访问)
*/
async function testPublicBrowsing(browser) {
console.log('\n【步骤7A】测试未登录公开浏览...');
const cleanContext = await browser.newContext({
viewport: { width: 750, height: 1334 },
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15'
});
const cleanPage = await cleanContext.newPage();
// Mock API(公开页面也需要获取数据)
await setupApiMocks(cleanPage);
try {
// 首页 —— 应能正常访问
await cleanPage.goto(MALL_URL, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
await cleanPage.waitForTimeout(2000);
let currentUrl = cleanPage.url();
let notRedirected = !currentUrl.includes('/login');
logTest('未登录访问首页', notRedirected, `URL: ${currentUrl}`);
await cleanPage.screenshot({ path: 'screenshots/mall-public-home.png', fullPage: true }).catch(() => {});
// 分类页 —— 应能正常访问
await cleanPage.goto(MALL_URL + '/category', { waitUntil: 'networkidle', timeout: 10000 }).catch(() => {});
await cleanPage.waitForTimeout(1000);
currentUrl = cleanPage.url();
notRedirected = currentUrl.includes('/category');
logTest('未登录访问分类页', notRedirected, `URL: ${currentUrl}`);
// 商品详情页 —— 应能正常访问
await cleanPage.goto(MALL_URL + '/product/1', { waitUntil: 'networkidle', timeout: 10000 }).catch(() => {});
await cleanPage.waitForTimeout(1000);
currentUrl = cleanPage.url();
notRedirected = currentUrl.includes('/product/1');
logTest('未登录访问商品详情', notRedirected, `URL: ${currentUrl}`);
await cleanPage.screenshot({ path: 'screenshots/mall-public-product.png', fullPage: true }).catch(() => {});
// 搜索页 —— 应能正常访问
await cleanPage.goto(MALL_URL + '/search', { waitUntil: 'networkidle', timeout: 10000 }).catch(() => {});
await cleanPage.waitForTimeout(1000);
currentUrl = cleanPage.url();
notRedirected = currentUrl.includes('/search');
logTest('未登录访问搜索页', notRedirected, `URL: ${currentUrl}`);
} finally {
await cleanContext.close();
}
}
/**
* 测试 7B:未登录访问受保护页面(应重定向到登录页)
*/
async function testAuthGuard(browser) {
console.log('\n【步骤7B】测试路由守卫(受保护页面重定向)...');
const cleanContext = await browser.newContext({
viewport: { width: 750, height: 1334 },
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15'
});
const cleanPage = await cleanContext.newPage();
try {
const protectedRoutes = [
{ path: '/cart', name: '购物车' },
{ path: '/orders', name: '订单列表' },
{ path: '/member', name: '会员页' },
{ path: '/address', name: '收货地址' },
{ path: '/checkout?cart_item_ids=1', name: '确认订单' }
];
for (const route of protectedRoutes) {
await cleanPage.goto(MALL_URL + route.path, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
await cleanPage.waitForTimeout(2000);
const currentUrl = cleanPage.url();
const redirectedToLogin = currentUrl.includes('/login');
logTest(`未登录→${route.name}重定向`, redirectedToLogin, `URL: ${currentUrl}`);
}
await cleanPage.screenshot({ path: 'screenshots/mall-auth-redirect.png', fullPage: true }).catch(() => {});
} finally {
await cleanContext.close();
}
}
/**
* 测试 8:页面响应式布局
*/
async function testResponsiveLayout(page) {
console.log('\n【步骤8】测试移动端响应式布局...');
// 设置移动端视口
await page.setViewportSize({ width: 375, height: 812 });
await page.goto(MALL_URL, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
await page.waitForTimeout(1500);
await page.screenshot({ path: 'screenshots/mall-mobile-375.png', fullPage: true }).catch(() => {});
// 检查元素在小屏幕上仍然可见
const searchBarVisible = await page.locator('.search-bar').isVisible().catch(() => false);
logTest('375px宽搜索栏', searchBarVisible);
const tabbarVisible = await page.locator('.mall-tabbar').isVisible().catch(() => false);
logTest('375px宽TabBar', tabbarVisible);
// 设置更宽的视口
await page.setViewportSize({ width: 750, height: 1024 });
await page.waitForTimeout(500);
await page.screenshot({ path: 'screenshots/mall-mobile-750.png', fullPage: true }).catch(() => {});
const searchBarVisible2 = await page.locator('.search-bar').isVisible().catch(() => false);
logTest('750px宽搜索栏', searchBarVisible2);
// 恢复默认
await page.setViewportSize({ width: 750, height: 1334 });
}
/**
* 测试 9:直接 URL 访问各路由
*/
async function testDirectRouteAccess(page) {
console.log('\n【步骤9】测试直接URL访问各路由...');
const routes = [
{ path: '/', name: '首页', titleCheck: '健康商城' },
{ path: '/category', name: '分类页', titleCheck: '分类' },
{ path: '/cart', name: '购物车', titleCheck: '购物车' },
{ path: '/member', name: '会员页', titleCheck: '我的' },
{ path: '/search', name: '搜索页', titleCheck: '搜索' },
{ path: '/address', name: '地址管理', titleCheck: '地址' },
{ path: '/orders', name: '订单列表', titleCheck: '订单' },
{ path: '/product/1', name: '商品详情', titleCheck: '商品详情' },
{ path: '/checkout?cart_item_ids=1', name: '确认订单', titleCheck: '确认订单' },
];
for (const route of routes) {
await page.goto(MALL_URL + route.path, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
await page.waitForTimeout(800);
// 检查是否在 mall 域内(未被重定向到其他站点)
const currentUrl = page.url();
const stayedOnMall = currentUrl.startsWith(MALL_URL);
// 检查标题
const title = await page.locator('.header-title').first().textContent().catch(() => '');
const titleMatches = title?.includes(route.titleCheck);
logTest(`路由[${route.name}]`, stayedOnMall && titleMatches, `URL: ${currentUrl}, 标题: "${title}"`);
}
}
/**
* 主测试入口
*/
async function runTests() {
console.log('═══════════════════════════════════════════════════════════');
console.log(' 商城前端 (mall) 自动化测试');
console.log('═══════════════════════════════════════════════════════════');
console.log(`目标地址: ${MALL_URL}`);
console.log(`时间: ${new Date().toLocaleString()}\n`);
let browser;
try {
browser = await chromium.launch({
headless: false,
args: ['--no-sandbox']
});
const context = await browser.newContext({
viewport: { width: 750, height: 1334 },
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1'
});
const page = await context.newPage();
page.setDefaultTimeout(10000);
// 阶段 0:设置登录态 & API Mock
await setupAuth(context, page);
await setupApiMocks(page);
// 阶段 1:商城首页
await testHomePage(page);
// 阶段 2:底部Tab导航
await testTabNavigation(page);
// 阶段 3:搜索页导航
await testSearchNavigation(page);
// 阶段 4:Header组件
await testHeaderComponents(page);
// 阶段 5:收货地址页
await testAddressPage(page);
// 阶段 6:订单页
await testOrdersPage(page);
// 阶段 6B:我的页面增强
await testMemberEnhancements(page);
// 阶段 6C:购物车增强
await testCartEnhancements(browser);
// 阶段 7A:未登录公开浏览
await testPublicBrowsing(browser);
// 阶段 7B:路由守卫(受保护页面重定向)
await testAuthGuard(browser);
// 阶段 8:响应式布局
await testResponsiveLayout(page);
// 阶段 9:直接URL访问
await testDirectRouteAccess(page);
} catch (err) {
console.error('\n❌ 测试执行出错:', err.message);
logTest('测试执行', false, err.message);
} finally {
if (browser) {
await browser.close();
}
}
// 输出汇总
console.log('\n═══════════════════════════════════════════════════════════');
console.log(' 测试结果摘要');
console.log('═══════════════════════════════════════════════════════════');
console.log(`通过: ${testResults.passed} 失败: ${testResults.failed}`);
console.log('───────────────────────────────────────────────────────────');
for (const t of testResults.tests) {
console.log(`${t.passed ? '✓' : '✗'} ${t.name}${t.detail ? ' - ' + t.detail : ''}`);
}
console.log('═══════════════════════════════════════════════════════════');
process.exit(testResults.failed > 0 ? 1 : 0);
}
runTests();