Browse Source

feat(mall): 补齐购物车与我的页面并完善自动化测试

补全购物车批量操作、会员页订单角标与失败重试,并修复积分/订单分页追加逻辑。
同步更新启动脚本选项、设计文档与 E2E 用例,mall.test.js 验证 62 项全部通过。

Co-authored-by: Cursor <cursoragent@cursor.com>
master
dark 1 month ago
parent
commit
46854ea550
  1. 36
      agents.md
  2. 14
      mall-design.md
  3. 18
      mall/src/stores/mall.ts
  4. 30
      mall/src/stores/member.ts
  5. 18
      mall/src/stores/order.ts
  6. 58
      mall/src/views/mall/CartView.vue
  7. 92
      mall/src/views/mall/MemberView.vue
  8. 43
      start.bat
  9. 26
      start.sh
  10. 183
      tests/mall.test.js
  11. BIN
      tests/screenshots/mall-address.png
  12. BIN
      tests/screenshots/mall-home.png
  13. BIN
      tests/screenshots/mall-member.png
  14. BIN
      tests/screenshots/mall-mobile-375.png
  15. BIN
      tests/screenshots/mall-orders.png
  16. BIN
      tests/screenshots/mall-public-home.png

36
agents.md

@ -657,3 +657,39 @@ cd backend && go run . # http://localhost:8080
- `mall/src/router/index.ts` - 路由守卫改造
- `mall/src/views/mall/ProductDetailView.vue` - 按钮登录检查
- `tests/mall.test.js` - 测试更新
---
### 2026-02-11: 商城页面补全 — 购物车 / 我的
**问题**: 购物车页和我的页关键交互缺失,影响日常使用。
**补全内容**:
1. **购物车页** (`mall/src/views/mall/CartView.vue`)
- 新增顶部操作区:`删除已选`、`清空购物车`
- 删除/清空前增加确认弹窗,完成后成功提示
- 操作中禁用按钮,避免重复点击导致重复请求
2. **购物车 Store** (`mall/src/stores/mall.ts`)
- 新增 `removeCartItems(ids)` 批量删除方法
- 使用并发删除后统一刷新购物车,减少页面闪烁
3. **我的页** (`mall/src/views/mall/MemberView.vue`)
- 新增订单状态角标(待支付/待发货/待收货/已完成)
- 新增会员信息加载失败提示与重试按钮
- 新增“退出登录”菜单项
- 增加退出确认弹窗,退出后跳转本地登录页
4. **会员 Store** (`mall/src/stores/member.ts`)
- 修复积分“加载更多”逻辑:第2页开始追加而非覆盖
- 增加重置状态方法 `resetMemberState()`
5. **订单 Store** (`mall/src/stores/order.ts`)
- 兼容 `page_info` 缺失场景
- 修复分页“加载更多”逻辑:第2页开始追加而非覆盖
**验证**:
- `mall` 执行 `npx vue-tsc --noEmit` 通过
- `tests/mall.test.js` 执行通过(62项,失败0)

14
mall-design.md

@ -1158,7 +1158,17 @@ storage:
---
> **文档版本**: v1.3
> **更新日期**: 2026-02-07
### v1.4 增量更新(2026-02-11)
- 购物车页补全批量操作:新增“删除已选”“清空购物车”交互和确认流程。
- 我的页补全账号操作:新增“退出登录”入口并接入本地登录页。
- 我的页补全订单状态总览:新增待支付/待发货/待收货/已完成角标统计。
- 会员信息请求失败时提供重试入口,提升弱网可恢复性。
- 修复会员积分记录分页加载:第 2 页起采用追加模式,避免“加载更多”覆盖已加载数据。
---
> **文档版本**: v1.4
> **更新日期**: 2026-02-11
> **创建日期**: 2026-02-01
> **关联项目**: 健康AI问询助手

18
mall/src/stores/mall.ts

@ -60,6 +60,12 @@ export const useMallStore = defineStore('mall', () => {
await fetchCart()
}
async function removeCartItems(ids: number[]) {
if (!ids.length) return
await Promise.all(ids.map((id) => deleteCartApi(id)))
await fetchCart()
}
async function toggleSelectAll(selected: boolean) {
const ids = cartItems.value.map(item => item.id)
if (ids.length > 0) {
@ -89,6 +95,14 @@ export const useMallStore = defineStore('mall', () => {
}
}
function resetMallState() {
cartItems.value = []
cartTotalCount.value = 0
cartSelectedCount.value = 0
cartTotalAmount.value = 0
cartLoading.value = false
}
return {
// 购物车
cartItems,
@ -103,11 +117,13 @@ export const useMallStore = defineStore('mall', () => {
addToCart,
updateCartItem,
removeCartItem,
removeCartItems,
toggleSelectAll,
clearCart,
// 分类
categories,
categoriesLoading,
fetchCategories
fetchCategories,
resetMallState
}
})

30
mall/src/stores/member.ts

@ -9,6 +9,7 @@ export const useMemberStore = defineStore('member', () => {
const pointsRecords = ref<PointsRecord[]>([])
const pageInfo = ref<PageInfo>({ total: 0, page: 1, page_size: 10 })
const loading = ref(false)
const memberLoadFailed = ref(false)
const levelInfo = computed(() => {
const level = memberInfo.value?.level || 'normal'
@ -27,10 +28,14 @@ export const useMemberStore = defineStore('member', () => {
async function fetchMemberInfo() {
loading.value = true
memberLoadFailed.value = false
try {
memberInfo.value = await getMemberInfoApi() as unknown as MemberInfo
return true
} catch {
// 错误已在拦截器中处理
memberLoadFailed.value = true
return false
} finally {
loading.value = false
}
@ -42,9 +47,18 @@ export const useMemberStore = defineStore('member', () => {
const data = await getPointsRecordsApi(params) as unknown as {
records: PointsRecord[]
page_info: PageInfo
total?: number
page?: number
page_size?: number
}
const page = params?.page || 1
const records = data.records || []
pointsRecords.value = page > 1 ? [...pointsRecords.value, ...records] : records
pageInfo.value = data.page_info || {
total: data.total || 0,
page: data.page || page,
page_size: data.page_size || (params?.page_size || 10)
}
pointsRecords.value = data.records || []
pageInfo.value = data.page_info
} catch {
// 错误已在拦截器中处理
} finally {
@ -52,14 +66,24 @@ export const useMemberStore = defineStore('member', () => {
}
}
function resetMemberState() {
memberInfo.value = null
pointsRecords.value = []
pageInfo.value = { total: 0, page: 1, page_size: 10 }
memberLoadFailed.value = false
loading.value = false
}
return {
memberInfo,
pointsRecords,
pageInfo,
loading,
memberLoadFailed,
levelInfo,
upgradeProgress,
fetchMemberInfo,
fetchPointsRecords
fetchPointsRecords,
resetMemberState
}
})

18
mall/src/stores/order.ts

@ -21,9 +21,21 @@ export const useOrderStore = defineStore('order', () => {
async function fetchOrders(params?: { page?: number; page_size?: number; status?: string }) {
loading.value = true
try {
const data = await getOrdersApi(params) as unknown as { orders: Order[]; page_info: PageInfo }
orders.value = data.orders || []
pageInfo.value = data.page_info
const data = await getOrdersApi(params) as unknown as {
orders: Order[]
page_info?: PageInfo
total?: number
page?: number
page_size?: number
}
const page = params?.page || 1
const list = data.orders || []
orders.value = page > 1 ? [...orders.value, ...list] : list
pageInfo.value = data.page_info || {
total: data.total || 0,
page: data.page || page,
page_size: data.page_size || (params?.page_size || 10)
}
} catch {
// 错误已在拦截器中处理
} finally {

58
mall/src/views/mall/CartView.vue

@ -7,6 +7,13 @@
<el-icon :size="14" color="#999"><ArrowRight /></el-icon>
</div>
<div v-if="mallStore.cartItems.length > 0" class="cart-tools">
<el-button text type="danger" :disabled="mallStore.cartSelectedCount === 0 || operating" @click="handleDeleteSelected">
删除已选
</el-button>
<el-button text :disabled="operating" @click="handleClearCart">清空购物车</el-button>
</div>
<!-- 购物车列表 -->
<div v-if="mallStore.cartItems.length > 0" v-loading="mallStore.cartLoading" class="cart-list">
<div
@ -102,8 +109,9 @@
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useMallStore } from '@/stores/mall'
import { jumpToHealthAI } from '@/utils/healthAI'
import type { CartItem } from '@/types'
@ -111,6 +119,7 @@ import { Delete, ChatDotRound, ArrowRight, Picture } from '@element-plus/icons-v
const router = useRouter()
const mallStore = useMallStore()
const operating = ref(false)
onMounted(() => {
mallStore.fetchCart()
@ -133,6 +142,45 @@ function handleDelete(id: number) {
mallStore.removeCartItem(id)
}
async function handleDeleteSelected() {
const ids = mallStore.selectedItems.map((item: CartItem) => item.id)
if (!ids.length || operating.value) return
try {
await ElMessageBox.confirm(`确认删除已选中的 ${ids.length} 件商品吗?`, '提示', {
confirmButtonText: '删除',
cancelButtonText: '取消',
type: 'warning'
})
operating.value = true
await mallStore.removeCartItems(ids)
ElMessage.success('已删除选中商品')
} catch {
//
} finally {
operating.value = false
}
}
async function handleClearCart() {
if (operating.value || mallStore.cartItems.length === 0) return
try {
await ElMessageBox.confirm('确认清空购物车吗?', '提示', {
confirmButtonText: '清空',
cancelButtonText: '取消',
type: 'warning'
})
operating.value = true
await mallStore.clearCart()
ElMessage.success('购物车已清空')
} catch {
//
} finally {
operating.value = false
}
}
/** 跳转商品详情 */
function goProductDetail(productId: number) {
router.push(`/product/${productId}`)
@ -193,6 +241,14 @@ $border: #EBEDF0;
}
}
.cart-tools {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 8px;
padding: 0 12px 8px;
}
.cart-list { padding: 0 12px 12px; }
.cart-item {

92
mall/src/views/mall/MemberView.vue

@ -18,6 +18,12 @@
</div>
</div>
<div v-if="memberStore.memberLoadFailed" class="section-card load-failed-card">
<div class="load-failed-title">会员信息加载失败</div>
<div class="load-failed-desc">请检查网络后重试</div>
<el-button type="primary" plain @click="reloadMemberInfo">重新加载</el-button>
</div>
<!-- 会员等级卡片 -->
<div class="section-card level-card">
<div class="level-header">
@ -89,7 +95,15 @@
class="order-tab-item"
@click="router.push(`/orders?status=${tab.status}`)"
>
<div class="tab-icon-wrap">
<div class="tab-icon">{{ tab.icon }}</div>
<el-badge
v-if="orderStatusCounts[tab.status] > 0"
:value="orderStatusCounts[tab.status]"
:max="99"
class="order-tab-badge"
/>
</div>
<div class="tab-label">{{ tab.label }}</div>
</div>
</div>
@ -151,10 +165,12 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowRight } from '@element-plus/icons-vue'
import { useAuthStore } from '@/stores/auth'
import { useMemberStore } from '@/stores/member'
import { useMallStore } from '@/stores/mall'
import { getOrdersApi } from '@/api/mall'
import { MEMBER_LEVEL_MAP } from '@/types'
import { jumpToHealthAI, goToConstitutionResult, goToHealthAI } from '@/utils/healthAI'
@ -190,6 +206,12 @@ const orderTabs = [
{ icon: '🚚', label: '待收货', status: 'shipped' },
{ icon: '✅', label: '已完成', status: 'completed' }
]
const orderStatusCounts = ref<Record<string, number>>({
pending: 0,
paid: 0,
shipped: 0,
completed: 0
})
// ========== ==========
@ -220,6 +242,11 @@ const menuList = [
icon: '🏠',
label: '返回健康助手',
action: () => goToHealthAI()
},
{
icon: '🚪',
label: '退出登录',
action: () => handleLogout()
}
]
@ -271,11 +298,50 @@ function formatDiscount(discount?: number): string {
return (discount * 10).toFixed(1) + '折'
}
async function handleLogout() {
try {
await ElMessageBox.confirm('确认退出当前账号吗?', '提示', {
confirmButtonText: '退出登录',
cancelButtonText: '取消',
type: 'warning'
})
memberStore.resetMemberState()
mallStore.resetMallState()
authStore.logout()
ElMessage.success('已退出登录')
router.replace('/login')
} catch {
//
}
}
async function reloadMemberInfo() {
await memberStore.fetchMemberInfo()
}
async function fetchOrderStatusCounts() {
const statuses = orderTabs.map((tab) => tab.status)
const results = await Promise.all(statuses.map(async (status) => {
try {
const data = await getOrdersApi({ page: 1, page_size: 1, status }) as unknown as {
page_info?: { total?: number }
total?: number
}
const total = data?.page_info?.total ?? data?.total ?? 0
return [status, total] as const
} catch {
return [status, 0] as const
}
}))
orderStatusCounts.value = Object.fromEntries(results)
}
// ========== ==========
onMounted(() => {
memberStore.fetchMemberInfo()
mallStore.fetchCart()
fetchOrderStatusCounts()
})
</script>
@ -385,6 +451,22 @@ $radius: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
}
.load-failed-card {
text-align: center;
.load-failed-title {
font-size: 16px;
font-weight: 600;
color: $text;
}
.load-failed-desc {
margin: 6px 0 12px;
color: $text-hint;
font-size: 13px;
}
}
/* ===== 会员等级卡片 ===== */
.level-card {
margin-top: -8px;
@ -523,6 +605,10 @@ $radius: 12px;
}
}
.tab-icon-wrap {
position: relative;
}
.tab-icon {
font-size: 28px;
line-height: 1;
@ -533,6 +619,12 @@ $radius: 12px;
color: $text-secondary;
}
.order-tab-badge {
position: absolute;
top: -8px;
right: -12px;
}
/* ===== 功能菜单 ===== */
.menu-section {
padding: 6px 16px;

43
start.bat

@ -5,27 +5,37 @@ echo ========================================
echo Health AI Assistant - Launcher
echo ========================================
echo.
echo [1] Start Web (Vue 3)
echo [2] Start APP (React Native)
echo [3] Start Both (Web + APP)
echo [4] Exit
echo [1] Start Web (Vue 3 - 5173)
echo [2] Start Mall (Vue 3 - 5174)
echo [3] Start APP (React Native)
echo [4] Start Web + Mall
echo [5] Start Web + APP
echo [6] Exit
echo.
echo ----------------------------------------
echo Test Account: 13800138000 / 123456
echo ----------------------------------------
echo.
set /p choice=Select option [1-4]:
set /p choice=Select option [1-6]:
if "%choice%"=="1" goto startweb
if "%choice%"=="2" goto startapp
if "%choice%"=="3" goto startall
if "%choice%"=="4" exit /b 0
if "%choice%"=="2" goto startmall
if "%choice%"=="3" goto startapp
if "%choice%"=="4" goto startwebmall
if "%choice%"=="5" goto startwebapp
if "%choice%"=="6" exit /b 0
goto :eof
:startweb
echo Starting Web dev server...
cd /d "%~dp0web"
npm run dev
cd /d "%~dp0"
node scripts/dev.js web
goto :eof
:startmall
echo Starting Mall dev server...
cd /d "%~dp0"
node scripts/dev.js mall
goto :eof
:startapp
@ -34,13 +44,18 @@ cd /d "%~dp0app"
npx expo start
goto :eof
:startall
echo Starting both servers...
start cmd /k "cd /d %~dp0web && npm run dev"
:startwebmall
echo Starting Web + Mall...
cd /d "%~dp0"
node scripts/dev.js
goto :eof
:startwebapp
echo Starting Web + APP in new windows...
start cmd /k "cd /d %~dp0 && node scripts/dev.js web"
timeout /t 2 /nobreak >nul
start cmd /k "cd /d %~dp0app && npx expo start --web"
echo.
echo Servers started in new windows!
echo Web: http://localhost:5173
echo APP: http://localhost:8081
pause

26
start.sh

@ -8,28 +8,40 @@ echo "========================================"
echo " Health AI Assistant - Launcher"
echo "========================================"
echo ""
echo " [1] Start Web (Vue 3)"
echo " [2] Start APP (React Native)"
echo " [3] Exit"
echo " [1] Start Web (Vue 3 - 5173)"
echo " [2] Start Mall (Vue 3 - 5174)"
echo " [3] Start APP (React Native)"
echo " [4] Start Web + Mall"
echo " [5] Exit"
echo ""
echo "----------------------------------------"
echo "Test Account: 13800138000 / 123456"
echo "----------------------------------------"
echo ""
read -p "Select option [1-3]: " choice
read -p "Select option [1-5]: " choice
case $choice in
1)
echo "Starting Web dev server..."
cd "$SCRIPT_DIR/web"
npm run dev
cd "$SCRIPT_DIR"
node scripts/dev.js web
;;
2)
echo "Starting Mall dev server..."
cd "$SCRIPT_DIR"
node scripts/dev.js mall
;;
3)
echo "Starting APP dev server..."
cd "$SCRIPT_DIR/app"
npx expo start
;;
3)
4)
echo "Starting Web + Mall..."
cd "$SCRIPT_DIR"
node scripts/dev.js
;;
5)
exit 0
;;
*)

183
tests/mall.test.js

@ -171,10 +171,25 @@ async function setupApiMocks(page) {
});
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: [], total: 0, page: 1, page_size: 20 })
body: JSON.stringify({
orders: [],
page_info: { total, page: 1, page_size: 20 },
total,
page: 1,
page_size: 20
})
});
});
@ -191,10 +206,30 @@ async function setupApiMocks(page) {
});
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: [], total: 0, page: 1, page_size: 20 })
body: JSON.stringify({
records,
page_info: { total: all.length, page: pageNo, page_size: pageSize },
total: all.length,
page: pageNo,
page_size: pageSize
})
});
});
@ -353,13 +388,13 @@ async function testHeaderComponents(page) {
await page.goto(MALL_URL, { waitUntil: 'networkidle', timeout: 10000 }).catch(() => {});
await page.waitForTimeout(1000);
// 检查 header 购物车图标
const headerCartBadge = page.locator('.cart-badge');
const cartBadgeVisible = await headerCartBadge.isVisible().catch(() => false);
logTest('Header购物车图标', cartBadgeVisible);
// 检查 header 购物车图标(角标为空时不一定存在 .cart-badge)
const headerCartIcon = page.locator('.header-right .header-icon').nth(1);
const cartIconVisible = await headerCartIcon.isVisible().catch(() => false);
logTest('Header购物车图标', cartIconVisible);
// 点击购物车图标应跳转到购物车
await headerCartBadge.click();
await headerCartIcon.click();
await page.waitForTimeout(1000);
const urlAfterCart = page.url();
logTest('Header购物车跳转', urlAfterCart.includes('/cart'), `URL: ${urlAfterCart}`);
@ -432,6 +467,134 @@ async function testOrdersPage(page) {
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未登录公开浏览应能正常访问
*/
@ -630,6 +793,12 @@ async function runTests() {
// 阶段 6:订单页
await testOrdersPage(page);
// 阶段 6B:我的页面增强
await testMemberEnhancements(page);
// 阶段 6C:购物车增强
await testCartEnhancements(browser);
// 阶段 7A:未登录公开浏览
await testPublicBrowsing(browser);

BIN
tests/screenshots/mall-address.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

BIN
tests/screenshots/mall-home.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 122 KiB

BIN
tests/screenshots/mall-member.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 85 KiB

BIN
tests/screenshots/mall-mobile-375.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 74 KiB

BIN
tests/screenshots/mall-orders.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 19 KiB

BIN
tests/screenshots/mall-public-home.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Loading…
Cancel
Save