fix: route payment pushes to orders and prevent refresh crash

This commit is contained in:
2026-09-08 09:59:51 +08:00
parent 10249776ad
commit c4b0e7798b
10 changed files with 333 additions and 47 deletions
@@ -24,6 +24,19 @@ final class DepositOrderListViewModelTests: XCTestCase {
super.tearDown()
}
func testOrderNotificationResetsFilters() {
let viewModel = DepositOrderListViewModel()
viewModel.onSearchTextChange("123", api: OrderAPI(client: APIClient(environment: .testing, session: MockURLSession(results: []))))
viewModel.onFilterChange(tab: 18)
viewModel.onDateRangeSelected(start: Date(), end: Date())
viewModel.resetFiltersForOrderNotification()
XCTAssertEqual(viewModel.searchText, "")
XCTAssertEqual(viewModel.tabIndex, 0)
XCTAssertEqual(viewModel.filterLabel, "全部")
XCTAssertNil(viewModel.startTime)
XCTAssertNil(viewModel.endTime)
}
func testStoreFilterMapsPaidStatusToAPI20() {
XCTAssertEqual(DepositOrderListViewModel.apiOrderStatus(forTabIndex: 18), 20)
XCTAssertEqual(DepositOrderListViewModel.apiOrderStatus(forTabIndex: 30), 30)
@@ -72,3 +72,88 @@ private extension UIColor {
return UInt(r << 16 | g << 8 | b)
}
}
/// 回归通知刷新时表格仍请求旧行的场景,使用 mock 数据覆盖两种订单页面。
@MainActor
final class OrderNotificationListSnapshotTests: XCTestCase {
private var appStore: AppStore!
private var suiteName: String!
override func setUp() {
super.setUp()
suiteName = "OrderNotificationListSnapshotTests.\(UUID().uuidString)"
appStore = AppStore(defaults: UserDefaults(suiteName: suiteName)!)
appStore.session.configureTestAccount(userId: "push-list-test", accountType: .storeUser, scenicId: 10)
appStore.session.currentStoreId = 20
}
override func tearDown() {
UserDefaults.standard.removePersistentDomain(forName: suiteName)
appStore = nil
super.tearDown()
}
func testPhotographerRowsRemainReadableWhileNotificationRefreshClearsModel() async throws {
try await verifyRefreshUsesSnapshot(storeMode: false)
}
func testDepositRowsRemainReadableWhileNotificationRefreshClearsModel() async throws {
try await verifyRefreshUsesSnapshot(storeMode: true)
}
private func verifyRefreshUsesSnapshot(storeMode: Bool) async throws {
appStore.session.roleCode = storeMode ? AppRoleCode.storeAdmin.rawValue : AppRoleCode.photographer.rawValue
let initial = Data(#"{"code":100000,"msg":"success","data":{"list":[{"order_number":"push-order-a","order_status":30},{"order_number":"push-order-b","order_status":30}],"total":2}}"#.utf8)
let empty = Data(#"{"code":100000,"msg":"success","data":{"list":[],"total":0}}"#.utf8)
let session = MockURLSession(responses: [initial, empty, initial])
let api = OrderAPI(client: APIClient(environment: .testing, session: session))
let orders = OrderListViewModel(appStore: appStore)
let deposits = DepositOrderListViewModel(appStore: appStore)
let controller = OrdersViewController(orderAPI: api, orderListViewModel: orders, depositViewModel: deposits, appStore: appStore)
controller.openFromOrderNotification()
let list = try XCTUnwrap(controller.view.subviews.first { $0 is OrderListView } as? OrderListView)
let table = list.tableView
await waitForRows(2, in: table)
let source = try XCTUnwrap(table.dataSource)
// 保持表格的旧快照,精确模拟模型已清空、主线程 UI 回调尚未执行的窗口。
let notify = storeMode ? deposits.onStateChange : orders.onStateChange
if storeMode {
deposits.onStateChange = nil
deposits.resetFiltersForOrderNotification()
await deposits.refreshOrderList(api: api)
XCTAssertTrue(deposits.orderList.isEmpty)
} else {
orders.onStateChange = nil
orders.resetFiltersForOrderNotification()
await orders.refreshOrderList(api: api)
XCTAssertTrue(orders.orderList.isEmpty)
}
XCTAssertEqual(source.tableView(table, numberOfRowsInSection: 0), 2)
for index in 0..<2 {
let cell = source.tableView(table, cellForRowAt: IndexPath(row: index, section: 0))
if storeMode {
XCTAssertTrue(cell is DepositOrderCardCell)
} else {
XCTAssertTrue(cell is OrderCardCell)
}
}
if storeMode { deposits.onStateChange = notify } else { orders.onStateChange = notify }
notify?()
await waitForRows(0, in: table)
// 已打开的订单页再次点击通知,仍能加载完整列表。
controller.openFromOrderNotification()
await waitForRows(2, in: table)
XCTAssertEqual(session.requests.count, 3)
}
private func waitForRows(_ count: Int, in table: UITableView) async {
let applied = expectation(for: NSPredicate { _, _ in
MainActor.assumeIsolated {
table.numberOfRows(inSection: 0) == count
}
}, evaluatedWith: nil)
await fulfillment(of: [applied], timeout: 3)
}
}
@@ -20,6 +20,20 @@ final class OrderListViewModelTests: XCTestCase {
super.tearDown()
}
func testOrderNotificationResetsFilters() {
let viewModel = OrderListViewModel()
viewModel.onSearchTextChange("13800138000")
viewModel.onFilterChange(orderStatus: 18, isRefined: 1)
viewModel.onDateRangeSelected(start: Date(), end: Date())
viewModel.resetFiltersForOrderNotification()
XCTAssertEqual(viewModel.searchText, "")
XCTAssertEqual(viewModel.tabIndex, 0)
XCTAssertEqual(viewModel.filterLabel, "全部")
XCTAssertNil(viewModel.startTime)
XCTAssertNil(viewModel.endTime)
XCTAssertEqual(viewModel.rTabIndex, 0)
}
func testOrderStatusQueryMapping() {
XCTAssertNil(OrderListQueryBuilder.orderStatusQuery(tabIndex: 0))
XCTAssertEqual(OrderListQueryBuilder.orderStatusQuery(tabIndex: 18), "18")
+34 -4
View File
@@ -27,10 +27,10 @@ final class PushNotificationTests: XCTestCase {
super.tearDown()
}
func testPayloadRoutesPaymentTypesToTheirPages() {
func testPayloadRoutesOrderAndScanTypesToTheirPages() {
XCTAssertEqual(
PushPayload(userInfo: ["type": 1, "route": "message_center"]).destination,
.paymentRecord
.orders
)
XCTAssertEqual(
PushPayload(userInfo: ["type": "6", "action": "task_management"]).destination,
@@ -38,6 +38,36 @@ final class PushNotificationTests: XCTestCase {
)
}
func testOrderTypesSupportNumbersStringsAndVendorWrappers() {
for type in [1, 5] {
XCTAssertEqual(PushPayload(userInfo: ["type": type]).destination, .orders)
XCTAssertEqual(PushPayload(userInfo: ["type": "\(type)"]).destination, .orders)
XCTAssertEqual(PushPayload(userInfo: ["extras": "{\"type\":\(type)}"]).destination, .orders)
XCTAssertEqual(PushPayload(values: ["type": "\(type)"]).destination, .orders)
}
}
func testOrderNotificationReturnsToOrdersRootAndDeduplicatesPendingRoute() {
let coordinator = PushRouteCoordinator(appStore: appStore)
let window = UIWindow(frame: UIScreen.main.bounds)
coordinator.attach(window: window)
coordinator.handle(destination: .orders, requestIdentifier: "order-request")
authenticate(userID: "100")
let mainTab = MainTabBarController()
mainTab.loadViewIfNeeded()
mainTab.selectTab(.orders)
let ordersNavigation = mainTab.selectedViewController as? TabNavigationController
ordersNavigation?.pushViewController(UIViewController(), animated: false)
mainTab.selectTab(.home)
window.rootViewController = mainTab
coordinator.routePendingIfPossible()
XCTAssertTrue(mainTab.selectedViewController === ordersNavigation)
XCTAssertEqual(ordersNavigation?.viewControllers.count, 1)
XCTAssertTrue(ordersNavigation?.topViewController is OrdersViewController)
coordinator.handle(destination: .messageCenter, requestIdentifier: "order-request")
XCTAssertTrue(mainTab.selectedViewController === ordersNavigation)
}
func testPayloadIgnoresNonTypeRoutingFieldsAndOtherTypesFallBackToMessageCenter() {
let conflictingFields: [AnyHashable: Any] = [
"type": 2,
@@ -46,7 +76,7 @@ final class PushNotificationTests: XCTestCase {
"action": "收款",
]
[0, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 100, 999].forEach { type in
[0, 2, 3, 4, 7, 8, 9, 10, 11, 12, 100, 999].forEach { type in
XCTAssertEqual(PushPayload(userInfo: ["type": type]).destination, .messageCenter)
}
XCTAssertEqual(PushPayload(userInfo: conflictingFields).destination, .messageCenter)
@@ -220,7 +250,7 @@ final class PushNotificationTests: XCTestCase {
payload: PushPayload(userInfo: ["type": 1, "route": "task_management"]),
requestIdentifier: "request-1"
)
XCTAssertEqual(router.destinations, [.paymentRecord])
XCTAssertEqual(router.destinations, [.orders])
}
func testRemoteNotificationNotifiesUnreadMessageStateChange() async {