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.
617 lines
26 KiB
617 lines
26 KiB
/**
|
|
* 商城前端 · 真实数据端到端测试
|
|
*
|
|
* 前置条件:
|
|
* 1. 后端 API 运行中:http://localhost:8080
|
|
* 2. 商城前端运行中:http://localhost:5174
|
|
*
|
|
* 运行:node tests/mall-real.test.js
|
|
*
|
|
* 测试流程:
|
|
* 真实登录 → 首页(真实分类+商品) → 分类浏览 → 商品详情 → SKU选择 →
|
|
* 加入购物车 → 购物车操作 → 地址管理(CRUD) → 订单查看 → 会员中心
|
|
*/
|
|
const { chromium } = require('playwright');
|
|
const http = require('http');
|
|
|
|
const API_URL = 'http://localhost:8080';
|
|
const MALL_URL = 'http://localhost:5174';
|
|
const HEALTH_AI_URL = 'http://localhost:5173';
|
|
const TEST_PHONE = '13800138000';
|
|
const TEST_PASSWORD = '123456';
|
|
|
|
const testResults = { passed: 0, failed: 0, tests: [] };
|
|
|
|
function logTest(name, passed, detail = '') {
|
|
const status = passed ? '✓' : '✗';
|
|
console.log(`${status} ${name}${detail ? ': ' + detail : ''}`);
|
|
testResults.tests.push({ name, passed, detail });
|
|
if (passed) testResults.passed++;
|
|
else testResults.failed++;
|
|
}
|
|
|
|
/** HTTP 请求工具 */
|
|
function apiRequest(method, path, data, token) {
|
|
return new Promise((resolve, reject) => {
|
|
const url = new URL(path, API_URL);
|
|
const body = data ? JSON.stringify(data) : null;
|
|
const headers = { 'Content-Type': 'application/json' };
|
|
if (token) headers['Authorization'] = token;
|
|
if (body) headers['Content-Length'] = Buffer.byteLength(body);
|
|
const options = {
|
|
hostname: url.hostname,
|
|
port: url.port,
|
|
path: url.pathname + url.search,
|
|
method,
|
|
headers
|
|
};
|
|
const req = http.request(options, (res) => {
|
|
let raw = '';
|
|
res.on('data', (c) => raw += c);
|
|
res.on('end', () => {
|
|
try { resolve(JSON.parse(raw)); } catch { resolve(raw); }
|
|
});
|
|
});
|
|
req.on('error', reject);
|
|
if (body) req.write(body);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
// ─── 步骤 0:通过 API 获取真实 Token ───────────────────
|
|
async function getRealToken() {
|
|
console.log('\n【步骤0】通过后端 API 登录获取真实 Token...');
|
|
const resp = await apiRequest('POST', '/api/auth/login', {
|
|
phone: TEST_PHONE,
|
|
password: TEST_PASSWORD
|
|
});
|
|
|
|
const token = resp?.data?.token || resp?.token;
|
|
const user = resp?.data?.user || resp?.user;
|
|
|
|
if (!token) {
|
|
console.error(' 登录响应:', JSON.stringify(resp).slice(0, 200));
|
|
logTest('API登录', false, '未获取到 token');
|
|
return null;
|
|
}
|
|
|
|
logTest('API登录', true, `用户: ${user?.nickname || user?.phone}, Token: ${token.slice(0, 20)}...`);
|
|
return { token, user };
|
|
}
|
|
|
|
// ─── 步骤 1:首页 — 真实分类 & 商品 ───────────────────
|
|
async function testHomePageReal(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-real-home.png', fullPage: true }).catch(() => {});
|
|
|
|
// 标题
|
|
const headerTitle = await page.locator('.header-title').first().textContent().catch(() => '');
|
|
logTest('首页标题', headerTitle?.includes('健康商城'), `"${headerTitle}"`);
|
|
|
|
// 分类导航 — 应该从真实 API 加载
|
|
const categoryItems = page.locator('.category-nav .category-item:not(.placeholder)');
|
|
const catCount = await categoryItems.count();
|
|
logTest('真实分类加载', catCount > 0, `加载了 ${catCount} 个分类`);
|
|
|
|
// 获取分类名称
|
|
if (catCount > 0) {
|
|
const firstCatName = await categoryItems.first().locator('.category-name').textContent().catch(() => '');
|
|
logTest('分类名称', firstCatName?.length > 0, `首个分类: "${firstCatName}"`);
|
|
}
|
|
|
|
// 热销商品 — 应该从真实 API 加载
|
|
const productCards = page.locator('.product-grid .product-card');
|
|
const productCount = await productCards.count();
|
|
logTest('真实商品加载', productCount > 0, `加载了 ${productCount} 个商品`);
|
|
|
|
// 商品名称 & 价格
|
|
if (productCount > 0) {
|
|
const firstName = await productCards.first().locator('.product-name').textContent().catch(() => '');
|
|
const firstPrice = await productCards.first().locator('.product-price').textContent().catch(() => '');
|
|
logTest('商品名称', firstName?.length > 0, `"${firstName?.trim()}"`);
|
|
logTest('商品价格', firstPrice?.includes('¥'), `${firstPrice?.trim()}`);
|
|
|
|
// 健康标签
|
|
const tags = productCards.first().locator('.product-tags .el-tag');
|
|
const tagCount = await tags.count();
|
|
logTest('健康标签', tagCount >= 0, `${tagCount} 个标签`);
|
|
}
|
|
|
|
// 体质推荐卡片
|
|
const constitutionCard = page.locator('.constitution-card');
|
|
logTest('体质推荐卡片', await constitutionCard.isVisible().catch(() => false));
|
|
|
|
// Banner
|
|
logTest('Banner轮播', await page.locator('.banner-section').isVisible().catch(() => false));
|
|
}
|
|
|
|
// ─── 步骤 2:分类页 — 浏览真实分类 ────────────────────
|
|
async function testCategoryPage(page) {
|
|
console.log('\n【步骤2】分类页 — 浏览真实分类...');
|
|
|
|
// 点击首页的第一个分类
|
|
const firstCat = page.locator('.category-nav .category-item:not(.placeholder)').first();
|
|
if (await firstCat.isVisible().catch(() => false)) {
|
|
const catName = await firstCat.locator('.category-name').textContent().catch(() => '');
|
|
await firstCat.click();
|
|
await page.waitForTimeout(2000);
|
|
await page.screenshot({ path: 'screenshots/mall-real-category.png', fullPage: true }).catch(() => {});
|
|
|
|
const url = page.url();
|
|
logTest('分类跳转', url.includes('/category/'), `点击: "${catName}", URL: ${url}`);
|
|
}
|
|
|
|
// 通过底部导航去分类页
|
|
await page.goto(MALL_URL + '/category', { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
await page.screenshot({ path: 'screenshots/mall-real-category-main.png', fullPage: true }).catch(() => {});
|
|
|
|
const title = await page.locator('.header-title').first().textContent().catch(() => '');
|
|
logTest('分类页标题', title?.includes('分类'), `"${title}"`);
|
|
}
|
|
|
|
// ─── 步骤 3:商品详情 — 真实数据 ──────────────────────
|
|
async function testProductDetailReal(page) {
|
|
console.log('\n【步骤3】商品详情 — 真实数据...');
|
|
|
|
// 回首页点击第一个商品
|
|
await page.goto(MALL_URL, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
|
|
const firstProduct = page.locator('.product-grid .product-card').first();
|
|
if (await firstProduct.isVisible().catch(() => false)) {
|
|
await firstProduct.click();
|
|
await page.waitForTimeout(3000);
|
|
await page.screenshot({ path: 'screenshots/mall-real-product-detail.png', fullPage: true }).catch(() => {});
|
|
|
|
const url = page.url();
|
|
logTest('商品详情跳转', url.includes('/product/'), `URL: ${url}`);
|
|
|
|
// 检查商品名称
|
|
const prodName = await page.locator('.product-name').first().textContent().catch(() => '');
|
|
logTest('商品详情名称', prodName?.length > 0, `"${prodName?.trim()}"`);
|
|
|
|
// 价格
|
|
const price = await page.locator('.current-price').first().textContent().catch(() => '');
|
|
logTest('商品详情价格', price?.includes('¥'), `${price?.trim()}`);
|
|
|
|
// 库存/销量
|
|
const salesRow = await page.locator('.sales-row').first().textContent().catch(() => '');
|
|
logTest('库存销量信息', salesRow?.length > 0, `${salesRow?.trim()}`);
|
|
|
|
// 体质标签
|
|
const constitutionTags = page.locator('.constitution-tags .el-tag');
|
|
const ctCount = await constitutionTags.count();
|
|
logTest('适合体质标签', ctCount > 0, `${ctCount} 个体质标签`);
|
|
|
|
// SKU 选择
|
|
const skuItems = page.locator('.sku-list .sku-item');
|
|
const skuCount = await skuItems.count();
|
|
logTest('SKU规格选项', skuCount > 0, `${skuCount} 个规格`);
|
|
|
|
// 选择第一个 SKU
|
|
if (skuCount > 0) {
|
|
const skuName = await skuItems.first().textContent().catch(() => '');
|
|
await skuItems.first().click();
|
|
await page.waitForTimeout(500);
|
|
|
|
const isActive = await skuItems.first().evaluate(el => el.classList.contains('active'));
|
|
logTest('SKU选中状态', isActive, `选中: "${skuName?.trim()}"`);
|
|
await page.screenshot({ path: 'screenshots/mall-real-sku-selected.png', fullPage: true }).catch(() => {});
|
|
}
|
|
|
|
// 功效/成分信息
|
|
const efficacy = await page.locator('.info-block').first().textContent().catch(() => '');
|
|
logTest('功效说明', efficacy?.length > 0, `${efficacy?.trim().slice(0, 50)}...`);
|
|
|
|
// 底部操作栏
|
|
const bottomBar = page.locator('.bottom-bar');
|
|
logTest('底部操作栏', await bottomBar.isVisible().catch(() => false));
|
|
|
|
const cartBtn = page.locator('.cart-btn');
|
|
logTest('加入购物车按钮', await cartBtn.isVisible().catch(() => false));
|
|
|
|
const buyBtn = page.locator('.buy-btn');
|
|
logTest('立即购买按钮', await buyBtn.isVisible().catch(() => false));
|
|
|
|
return url; // 返回详情页 URL 供后续使用
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// ─── 步骤 4:加入购物车 & 购物车操作 ──────────────────
|
|
async function testCartFlow(page, token) {
|
|
console.log('\n【步骤4】加入购物车 & 购物车操作...');
|
|
|
|
// 先通过 API 清理购物车(确保干净状态)
|
|
try {
|
|
await apiRequest('DELETE', '/api/mall/cart/clear', null, token);
|
|
} catch { /* 忽略 */ }
|
|
|
|
// 在商品详情页点击"加入购物车"
|
|
const cartBtn = page.locator('.cart-btn');
|
|
if (await cartBtn.isVisible().catch(() => false)) {
|
|
await cartBtn.click();
|
|
await page.waitForTimeout(2000);
|
|
|
|
// 检查是否出现成功提示
|
|
const successMsg = page.locator('.el-message--success');
|
|
const msgVisible = await successMsg.isVisible({ timeout: 5000 }).catch(() => false);
|
|
logTest('加入购物车提示', msgVisible, msgVisible ? '已加入购物车' : '未显示成功提示');
|
|
await page.screenshot({ path: 'screenshots/mall-real-add-cart.png', fullPage: true }).catch(() => {});
|
|
}
|
|
|
|
// 导航到购物车页
|
|
await page.goto(MALL_URL + '/cart', { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
await page.screenshot({ path: 'screenshots/mall-real-cart.png', fullPage: true }).catch(() => {});
|
|
|
|
// 检查购物车中有商品
|
|
const cartItems = page.locator('.cart-item');
|
|
const cartCount = await cartItems.count();
|
|
logTest('购物车商品', cartCount > 0, `${cartCount} 个商品`);
|
|
|
|
if (cartCount > 0) {
|
|
// 商品名称
|
|
const itemName = await cartItems.first().locator('.item-name').textContent().catch(() => '');
|
|
logTest('购物车商品名称', itemName?.length > 0, `"${itemName?.trim()}"`);
|
|
|
|
// 价格
|
|
const itemPrice = await cartItems.first().locator('.item-price').textContent().catch(() => '');
|
|
logTest('购物车商品价格', itemPrice?.includes('¥'), `${itemPrice?.trim()}`);
|
|
|
|
// 数量控制
|
|
const quantityInput = cartItems.first().locator('.el-input-number');
|
|
logTest('数量控制器', await quantityInput.isVisible().catch(() => false));
|
|
|
|
// 底部结算栏
|
|
const footer = page.locator('.cart-footer');
|
|
logTest('底部结算栏', await footer.isVisible().catch(() => false));
|
|
|
|
// 合计金额
|
|
const total = await page.locator('.total-price').first().textContent().catch(() => '');
|
|
logTest('合计金额', total?.includes('¥'), `${total?.trim()}`);
|
|
|
|
// 结算按钮
|
|
const checkoutBtn = page.locator('.checkout-btn');
|
|
logTest('去结算按钮', await checkoutBtn.isVisible().catch(() => false));
|
|
|
|
// 全选
|
|
const selectAll = page.locator('.footer-left .el-checkbox');
|
|
logTest('全选按钮', await selectAll.isVisible().catch(() => false));
|
|
}
|
|
|
|
// 通过 API 清理购物车
|
|
try {
|
|
await apiRequest('DELETE', '/api/mall/cart/clear', null, token);
|
|
} catch { /* 忽略 */ }
|
|
}
|
|
|
|
// ─── 步骤 5:地址管理 — 真实 CRUD ─────────────────────
|
|
async function testAddressReal(page, token) {
|
|
console.log('\n【步骤5】地址管理 — 真实 CRUD...');
|
|
|
|
await page.goto(MALL_URL + '/address', { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
await page.screenshot({ path: 'screenshots/mall-real-address.png', fullPage: true }).catch(() => {});
|
|
|
|
// 检查现有地址
|
|
const addrItems = page.locator('.address-item');
|
|
const addrCount = await addrItems.count();
|
|
logTest('现有地址数量', true, `${addrCount} 个地址`);
|
|
|
|
// 如果有地址,验证内容
|
|
if (addrCount > 0) {
|
|
const name = await addrItems.first().locator('.addr-name').textContent().catch(() => '');
|
|
const phone = await addrItems.first().locator('.addr-phone').textContent().catch(() => '');
|
|
const detail = await addrItems.first().locator('.addr-detail').textContent().catch(() => '');
|
|
logTest('地址收件人', name?.length > 0, `"${name}"`);
|
|
logTest('地址电话', phone?.length > 0, `"${phone}"`);
|
|
logTest('地址详情', detail?.length > 0, `"${detail?.trim().slice(0, 30)}..."`);
|
|
}
|
|
|
|
// 新增地址
|
|
const addBtn = page.locator('.add-btn');
|
|
await addBtn.click();
|
|
await page.waitForTimeout(1000);
|
|
|
|
const drawer = page.locator('.el-drawer');
|
|
logTest('新增地址抽屉', await drawer.isVisible({ timeout: 3000 }).catch(() => false));
|
|
|
|
// 填写表单
|
|
const formVisible = await page.locator('.address-form').isVisible().catch(() => false);
|
|
if (formVisible) {
|
|
// 填写收件人
|
|
const nameInput = page.locator('.address-form .el-input__inner').first();
|
|
await nameInput.fill('测试收件人');
|
|
await page.waitForTimeout(200);
|
|
|
|
// 填写手机号
|
|
const phoneInput = page.locator('.address-form .el-input__inner').nth(1);
|
|
await phoneInput.fill('13912345678');
|
|
await page.waitForTimeout(200);
|
|
|
|
// 填写省市区
|
|
const provinceInput = page.locator('.address-form .el-input__inner').nth(2);
|
|
await provinceInput.fill('浙江省');
|
|
const cityInput = page.locator('.address-form .el-input__inner').nth(3);
|
|
await cityInput.fill('杭州市');
|
|
const districtInput = page.locator('.address-form .el-input__inner').nth(4);
|
|
await districtInput.fill('西湖区');
|
|
await page.waitForTimeout(200);
|
|
|
|
// 填写详细地址
|
|
const detailInput = page.locator('.address-form .el-textarea__inner').first();
|
|
await detailInput.fill('文三路100号 E2E测试地址');
|
|
await page.waitForTimeout(200);
|
|
|
|
await page.screenshot({ path: 'screenshots/mall-real-address-fill.png', fullPage: true }).catch(() => {});
|
|
|
|
// 点击保存
|
|
const saveBtn = page.locator('.save-btn');
|
|
await saveBtn.click();
|
|
await page.waitForTimeout(3000);
|
|
|
|
// 检查是否保存成功(抽屉关闭 & 列表刷新)
|
|
const drawerClosed = !(await drawer.isVisible().catch(() => true));
|
|
logTest('地址保存', drawerClosed, drawerClosed ? '抽屉已关闭' : '抽屉仍打开');
|
|
|
|
// 等待列表刷新完成 — 等 address-item 出现(如果之前没有的话)
|
|
if (addrCount === 0) {
|
|
await page.locator('.address-item').first().waitFor({ timeout: 8000 }).catch(() => {});
|
|
} else {
|
|
await page.waitForTimeout(3000);
|
|
}
|
|
await page.screenshot({ path: 'screenshots/mall-real-address-saved.png', fullPage: true }).catch(() => {});
|
|
|
|
// 列表中是否有新地址
|
|
const newAddrCount = await page.locator('.address-item').count();
|
|
logTest('地址列表更新', newAddrCount > addrCount, `之前: ${addrCount}, 现在: ${newAddrCount}`);
|
|
|
|
// 查找并删除刚创建的测试地址(通过 API 清理)
|
|
try {
|
|
const resp = await apiRequest('GET', '/api/mall/addresses', null, token);
|
|
const addresses = resp?.addresses || resp || [];
|
|
const testAddr = addresses.find(a => a.detail_addr?.includes('E2E测试地址'));
|
|
if (testAddr) {
|
|
await apiRequest('DELETE', `/api/mall/addresses/${testAddr.id}`, null, token);
|
|
logTest('测试地址清理', true, `已删除 ID: ${testAddr.id}`);
|
|
}
|
|
} catch { /* 忽略清理错误 */ }
|
|
} else {
|
|
// 取消
|
|
const cancelBtn = page.locator('.cancel-btn');
|
|
await cancelBtn.click().catch(() => {});
|
|
await page.waitForTimeout(500);
|
|
}
|
|
}
|
|
|
|
// ─── 步骤 6:订单列表 — 真实数据 ──────────────────────
|
|
async function testOrdersReal(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-real-orders.png', fullPage: true }).catch(() => {});
|
|
|
|
const title = await page.locator('.header-title').first().textContent().catch(() => '');
|
|
logTest('订单页标题', title?.includes('订单'), `"${title}"`);
|
|
|
|
// 检查是否有订单(可能有,可能没有)
|
|
// 查找订单列表或空状态
|
|
const pageContent = await page.textContent('body').catch(() => '');
|
|
const hasOrders = pageContent.includes('订单') || pageContent.includes('order');
|
|
logTest('订单页渲染', hasOrders, '页面正常渲染');
|
|
}
|
|
|
|
// ─── 步骤 7:会员中心 — 真实数据 ──────────────────────
|
|
async function testMemberReal(page) {
|
|
console.log('\n【步骤7】会员中心 — 真实数据...');
|
|
|
|
await page.goto(MALL_URL + '/member', { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
await page.screenshot({ path: 'screenshots/mall-real-member.png', fullPage: true }).catch(() => {});
|
|
|
|
const title = await page.locator('.header-title').first().textContent().catch(() => '');
|
|
logTest('会员页标题', title?.includes('我的'), `"${title}"`);
|
|
|
|
// 检查会员信息是否加载
|
|
const pageContent = await page.textContent('body').catch(() => '');
|
|
|
|
// 检查积分等数据是否展示
|
|
const hasPoints = pageContent.includes('积分') || pageContent.includes('会员');
|
|
logTest('会员信息加载', hasPoints, hasPoints ? '积分/会员信息已显示' : '未找到会员信息');
|
|
|
|
// TabBar 应该显示
|
|
const tabbar = page.locator('.mall-tabbar');
|
|
logTest('会员页TabBar', await tabbar.isVisible().catch(() => false));
|
|
}
|
|
|
|
// ─── 步骤 8:搜索功能 — 真实搜索 ─────────────────────
|
|
async function testSearchReal(page) {
|
|
console.log('\n【步骤8】搜索功能 — 真实搜索...');
|
|
|
|
await page.goto(MALL_URL + '/search', { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(1500);
|
|
await page.screenshot({ path: 'screenshots/mall-real-search.png', fullPage: true }).catch(() => {});
|
|
|
|
const title = await page.locator('.header-title').first().textContent().catch(() => '');
|
|
logTest('搜索页标题', title?.includes('搜索'), `"${title}"`);
|
|
|
|
// 查找搜索输入框
|
|
const searchInput = page.locator('input[type="text"], input[placeholder*="搜索"], .search-input input').first();
|
|
const inputVisible = await searchInput.isVisible().catch(() => false);
|
|
|
|
if (inputVisible) {
|
|
await searchInput.fill('养生茶');
|
|
await page.waitForTimeout(500);
|
|
|
|
// 按 Enter 或点搜索按钮
|
|
await searchInput.press('Enter');
|
|
await page.waitForTimeout(3000);
|
|
await page.screenshot({ path: 'screenshots/mall-real-search-result.png', fullPage: true }).catch(() => {});
|
|
|
|
const resultContent = await page.textContent('body').catch(() => '');
|
|
const hasResults = resultContent.includes('养生') || resultContent.includes('茶');
|
|
logTest('搜索结果', hasResults, hasResults ? '找到相关商品' : '未找到搜索结果');
|
|
} else {
|
|
logTest('搜索输入框', false, '未找到搜索输入框');
|
|
}
|
|
}
|
|
|
|
// ─── 步骤 9:完整购物流程(加购 → 结算页) ──────────
|
|
async function testCheckoutFlow(page, token) {
|
|
console.log('\n【步骤9】完整购物流程 — 加购到结算...');
|
|
|
|
// 通过 API 添加商品到购物车
|
|
try {
|
|
await apiRequest('DELETE', '/api/mall/cart/clear', null, token);
|
|
await apiRequest('POST', '/api/mall/cart', { product_id: 1, quantity: 2 }, token);
|
|
logTest('API加入购物车', true, '商品ID: 1, 数量: 2');
|
|
} catch (e) {
|
|
logTest('API加入购物车', false, e.message);
|
|
return;
|
|
}
|
|
|
|
// 打开购物车页
|
|
await page.goto(MALL_URL + '/cart', { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
await page.screenshot({ path: 'screenshots/mall-real-cart-with-items.png', fullPage: true }).catch(() => {});
|
|
|
|
// 验证购物车有商品
|
|
const cartItems = page.locator('.cart-item');
|
|
const cartCount = await cartItems.count();
|
|
logTest('购物车有商品', cartCount > 0, `${cartCount} 个商品`);
|
|
|
|
if (cartCount > 0) {
|
|
// 点击去结算
|
|
const checkoutBtn = page.locator('.checkout-btn');
|
|
if (await checkoutBtn.isVisible().catch(() => false)) {
|
|
// 先勾选商品(如果未勾选)
|
|
const checkbox = cartItems.first().locator('.el-checkbox');
|
|
if (await checkbox.isVisible().catch(() => false)) {
|
|
// 检查是否已选中
|
|
const checked = await checkbox.evaluate(el =>
|
|
el.querySelector('.el-checkbox__input')?.classList.contains('is-checked')
|
|
).catch(() => false);
|
|
if (!checked) {
|
|
await checkbox.click();
|
|
await page.waitForTimeout(1000);
|
|
}
|
|
}
|
|
|
|
await checkoutBtn.click();
|
|
await page.waitForTimeout(3000);
|
|
await page.screenshot({ path: 'screenshots/mall-real-checkout.png', fullPage: true }).catch(() => {});
|
|
|
|
const checkoutUrl = page.url();
|
|
logTest('跳转结算页', checkoutUrl.includes('/checkout'), `URL: ${checkoutUrl}`);
|
|
|
|
// 结算页面验证
|
|
if (checkoutUrl.includes('/checkout')) {
|
|
const checkoutTitle = await page.locator('.page-title, .header-title').first().textContent().catch(() => '');
|
|
logTest('结算页标题', checkoutTitle?.includes('确认') || checkoutTitle?.includes('订单'), `"${checkoutTitle}"`);
|
|
|
|
// 地址卡片
|
|
const addrCard = page.locator('.address-card');
|
|
logTest('收货地址卡片', await addrCard.isVisible().catch(() => false));
|
|
}
|
|
}
|
|
}
|
|
|
|
// 清理购物车
|
|
try {
|
|
await apiRequest('DELETE', '/api/mall/cart/clear', null, token);
|
|
} catch { /* 忽略 */ }
|
|
}
|
|
|
|
// ─── 主入口 ──────────────────────────────────────────
|
|
async function runTests() {
|
|
console.log('═══════════════════════════════════════════════════════════');
|
|
console.log(' 商城前端 · 真实数据端到端测试');
|
|
console.log('═══════════════════════════════════════════════════════════');
|
|
console.log(`后端 API: ${API_URL}`);
|
|
console.log(`商城前端: ${MALL_URL}`);
|
|
console.log(`时间: ${new Date().toLocaleString()}\n`);
|
|
|
|
let browser;
|
|
try {
|
|
// ── 获取真实 Token ──
|
|
const auth = await getRealToken();
|
|
if (!auth) {
|
|
console.error('❌ 无法获取 Token,请确认后端已启动');
|
|
process.exit(1);
|
|
}
|
|
|
|
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'
|
|
});
|
|
|
|
// 注入真实 Token
|
|
await context.addInitScript(({ token, user }) => {
|
|
try {
|
|
localStorage.setItem('token', token);
|
|
localStorage.setItem('user', JSON.stringify(user));
|
|
} catch { /* ignore */ }
|
|
}, { token: auth.token, user: auth.user });
|
|
|
|
const page = await context.newPage();
|
|
page.setDefaultTimeout(10000);
|
|
|
|
// 记录 API 请求/响应用于调试
|
|
const apiLogs = [];
|
|
page.on('response', async (resp) => {
|
|
const url = resp.url();
|
|
if (url.includes('/api/mall/')) {
|
|
const status = resp.status();
|
|
apiLogs.push({ url: url.replace(API_URL, ''), status });
|
|
if (status >= 400) {
|
|
console.log(` ⚠ API ${status}: ${url.replace(API_URL, '')}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
// 执行测试
|
|
await testHomePageReal(page);
|
|
await testCategoryPage(page);
|
|
await testProductDetailReal(page);
|
|
await testCartFlow(page, auth.token);
|
|
await testAddressReal(page, auth.token);
|
|
await testOrdersReal(page);
|
|
await testMemberReal(page);
|
|
await testSearchReal(page);
|
|
await testCheckoutFlow(page, auth.token);
|
|
|
|
// 输出 API 请求汇总
|
|
console.log(`\n API 请求汇总: 共 ${apiLogs.length} 次, 失败: ${apiLogs.filter(l => l.status >= 400).length}`);
|
|
|
|
} 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();
|
|
|