Add custom main tab bar with global scan-to-verify flow.

Introduce configurable custom TabBar with order badge, central scanner routing to verification orders, and update the migration checklist for recently completed modules.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-23 10:52:45 +08:00
parent 3a11583729
commit 1d1eb46ed8
25 changed files with 740 additions and 34 deletions

View File

@ -0,0 +1,106 @@
//
// MainTabBadgeViewModelTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/23.
//
import XCTest
@testable import suixinkan
@MainActor
/// Tab
final class MainTabBadgeViewModelTests: XCTestCase {
///
func testMissingScenicClearsBadgeWithoutRequestingAPI() async {
let api = MockMainTabOrderService()
let viewModel = MainTabBadgeViewModel()
await viewModel.refreshPendingWriteOffCount(api: api, scenicId: nil, storeId: 66)
XCTAssertNil(viewModel.pendingWriteOffCount)
XCTAssertTrue(api.writeOffListCalls.isEmpty)
}
///
func testSuccessfulRefreshStoresPendingWriteOffCount() async {
let api = MockMainTabOrderService()
api.writeOffResponse = ListPayload<WriteOffOrderItem>(total: 8, list: [])
let viewModel = MainTabBadgeViewModel()
await viewModel.refreshPendingWriteOffCount(api: api, scenicId: 88, storeId: 66)
XCTAssertEqual(viewModel.pendingWriteOffCount, 8)
XCTAssertEqual(api.writeOffListCalls, [.init(scenicId: 88, storeId: 66, page: 1, pageSize: 1)])
}
/// 99
func testSuccessfulRefreshCapsPendingWriteOffCountAtNinetyNine() async {
let api = MockMainTabOrderService()
api.writeOffResponse = ListPayload<WriteOffOrderItem>(total: 120, list: [])
let viewModel = MainTabBadgeViewModel()
await viewModel.refreshPendingWriteOffCount(api: api, scenicId: 88, storeId: nil)
XCTAssertEqual(viewModel.pendingWriteOffCount, 99)
}
///
func testRefreshFailureClearsBadge() async {
let api = MockMainTabOrderService()
api.writeOffError = APIError.httpStatus(500, "server error")
let viewModel = MainTabBadgeViewModel()
await viewModel.refreshPendingWriteOffCount(api: api, scenicId: 88, storeId: 66)
XCTAssertNil(viewModel.pendingWriteOffCount)
XCTAssertEqual(api.writeOffListCalls.count, 1)
}
}
@MainActor
/// Tab
private final class MockMainTabOrderService: OrderServing {
struct WriteOffListCall: Equatable {
let scenicId: Int
let storeId: Int?
let page: Int
let pageSize: Int
}
var writeOffResponse = ListPayload<WriteOffOrderItem>(total: 0, list: [])
var writeOffError: Error?
private(set) var writeOffListCalls: [WriteOffListCall] = []
/// 使
func orderList(
scenicId: Int,
page: Int,
pageSize: Int,
orderStatus: Int?,
userPhone: String?,
startTime: String?,
endTime: String?,
isRefined: Int?,
isScenicAdmin: Bool
) async throws -> ListPayload<OrderEntity> {
ListPayload(total: 0, list: [])
}
///
func writeOffList(scenicId: Int, storeId: Int?, page: Int, pageSize: Int) async throws -> ListPayload<WriteOffOrderItem> {
writeOffListCalls.append(.init(scenicId: scenicId, storeId: storeId, page: page, pageSize: pageSize))
if let writeOffError {
throw writeOffError
}
return writeOffResponse
}
///
func writeOff(orderNumber: String) async throws {}
///
func storeOrderDetail(storeId: Int, orderNumber: String) async throws -> StoreOrderDetailResponse {
try TestFixture.payload(StoreOrderDetailResponse.self, named: "store_order_detail_success")
}
}

View File

@ -30,6 +30,38 @@ final class NavigationRouterTests: XCTestCase {
XCTAssertEqual(router.selectedOrdersEntry, .verificationOrders)
}
///
func testRouteToOrderVerificationStoresPendingScanCode() {
let router = AppRouter()
router.routeToOrderVerification(scannedCode: "order_number=VERIFY001")
XCTAssertEqual(router.selectedTab, .orders)
XCTAssertEqual(router.selectedOrdersEntry, .verificationOrders)
XCTAssertEqual(router.pendingOrderScanCode, "order_number=VERIFY001")
}
///
func testConsumePendingOrderScanCodeClearsValueAfterFirstRead() {
let router = AppRouter()
router.routeToOrderVerification(scannedCode: "VERIFY001")
XCTAssertEqual(router.consumePendingOrderScanCode(), "VERIFY001")
XCTAssertNil(router.consumePendingOrderScanCode())
}
///
func testResetClearsPendingOrderScanCode() {
let router = AppRouter()
router.routeToOrderVerification(scannedCode: "VERIFY001")
router.reset()
XCTAssertEqual(router.selectedTab, .home)
XCTAssertEqual(router.selectedOrdersEntry, .storeOrders)
XCTAssertNil(router.pendingOrderScanCode)
}
///
func testOrderRoutesCanBePushedIntoRouterPath() throws {
let router = RouterPath()