/** * 商城前端 · 真实数据端到端测试 * * 前置条件: * 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();