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
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();
|
|
|