fix: 优化相册素材状态与删除保护

This commit is contained in:
2026-08-17 16:04:03 +08:00
parent b127495e52
commit c34c4e1923
6 changed files with 254 additions and 81 deletions
@@ -1149,9 +1149,8 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase {
XCTAssertFalse(controller.view.allLabels().contains { $0.text == "刷新失败" })
}
func testMaterialCellShowsSemanticStatusBadgeWithoutOverlappingSelectionCheck() throws {
func testMaterialCellShowsIndependentPurchaseAndAIRetouchBadgesWithoutOverlappingSelectionCheck() throws {
let cases: [(TravelAlbumMaterial, String, UInt)] = [
(TravelAlbumMaterial(isPurchased: true), "已购", 0x475569),
(TravelAlbumMaterial(aiRetouchStatus: 1), "待处理", 0xB45309),
(TravelAlbumMaterial(aiRetouchStatus: 2), "修图中", 0x1D4ED8),
(TravelAlbumMaterial(aiRetouchStatus: 3), "AI已修", 0x047857),
@@ -1170,12 +1169,12 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase {
let badgeView = try XCTUnwrap(
cell.findSubview {
$0.accessibilityIdentifier == "travelAlbum.materialStatusBadge"
$0.accessibilityIdentifier == "travelAlbum.materialAIRetouchBadge"
}
)
let badgeLabel = try XCTUnwrap(
cell.findSubview {
$0.accessibilityIdentifier == "travelAlbum.materialStatusBadgeLabel"
$0.accessibilityIdentifier == "travelAlbum.materialAIRetouchBadgeLabel"
} as? UILabel
)
let selectionCheck = try XCTUnwrap(
@@ -1188,22 +1187,61 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase {
XCTAssertEqual(badgeLabel.text, expectedText)
XCTAssertEqual(badgeView.backgroundColor?.travelAlbumTestHexRGB, expectedColor)
XCTAssertLessThan(badgeView.frame.maxX, selectionCheck.frame.minX)
XCTAssertTrue(cell.accessibilityLabel?.contains("状态:\(expectedText)") == true)
XCTAssertTrue(cell.accessibilityLabel?.contains("修图状态:\(expectedText)") == true)
}
let combinedCell = TravelAlbumMaterialCell(frame: CGRect(x: 0, y: 0, width: 176, height: 220))
combinedCell.apply(
material: TravelAlbumMaterial(isPurchased: true, aiRetouchStatus: 2),
selectionMode: true,
selected: false
)
combinedCell.layoutIfNeeded()
let purchaseBadge = try XCTUnwrap(
combinedCell.findSubview {
$0.accessibilityIdentifier == "travelAlbum.materialPurchaseBadge"
}
)
let purchaseLabel = try XCTUnwrap(
combinedCell.findSubview {
$0.accessibilityIdentifier == "travelAlbum.materialPurchaseBadgeLabel"
} as? UILabel
)
let aiRetouchBadge = try XCTUnwrap(
combinedCell.findSubview {
$0.accessibilityIdentifier == "travelAlbum.materialAIRetouchBadge"
}
)
XCTAssertFalse(purchaseBadge.isHidden)
XCTAssertFalse(aiRetouchBadge.isHidden)
XCTAssertEqual(purchaseLabel.text, "已购")
XCTAssertEqual(purchaseBadge.backgroundColor?.travelAlbumTestHexRGB, 0x475569)
XCTAssertGreaterThan(purchaseBadge.frame.minY, aiRetouchBadge.frame.maxY)
XCTAssertEqual(purchaseBadge.frame.minX, 6, accuracy: 0.5)
XCTAssertEqual(purchaseBadge.frame.maxY, 170, accuracy: 0.5)
XCTAssertTrue(combinedCell.accessibilityLabel?.contains("购买状态:已购") == true)
XCTAssertTrue(combinedCell.accessibilityLabel?.contains("修图状态:修图中") == true)
let hiddenCell = TravelAlbumMaterialCell(frame: CGRect(x: 0, y: 0, width: 176, height: 220))
hiddenCell.apply(
material: TravelAlbumMaterial(aiRetouchStatus: 0, aiRetouchStatusName: "已上传"),
selectionMode: false,
selected: false
)
let hiddenBadge = try XCTUnwrap(
let hiddenPurchaseBadge = try XCTUnwrap(
hiddenCell.findSubview {
$0.accessibilityIdentifier == "travelAlbum.materialStatusBadge"
$0.accessibilityIdentifier == "travelAlbum.materialPurchaseBadge"
}
)
XCTAssertTrue(hiddenBadge.isHidden)
XCTAssertFalse(hiddenCell.accessibilityLabel?.contains("状态:") == true)
let hiddenAIRetouchBadge = try XCTUnwrap(
hiddenCell.findSubview {
$0.accessibilityIdentifier == "travelAlbum.materialAIRetouchBadge"
}
)
XCTAssertTrue(hiddenPurchaseBadge.isHidden)
XCTAssertTrue(hiddenAIRetouchBadge.isHidden)
XCTAssertFalse(hiddenCell.accessibilityLabel?.contains("购买状态:") == true)
XCTAssertFalse(hiddenCell.accessibilityLabel?.contains("修图状态:") == true)
}
private func waitUntil(_ condition: @escaping () -> Bool) async {
+27 -13
View File
@@ -399,34 +399,48 @@ final class TravelAlbumModelsTests: XCTestCase {
XCTAssertEqual(material.aiAtmosphereURL, "")
}
func testTravelAlbumMaterialBadgePriorityAndFallbacks() {
XCTAssertNil(TravelAlbumMaterial(aiRetouchStatus: 0).badgePresentation)
func testTravelAlbumMaterialPurchaseAndAIRetouchBadgesAreIndependent() {
XCTAssertNil(TravelAlbumMaterial().purchaseBadgePresentation)
XCTAssertEqual(
TravelAlbumMaterial(isPurchased: true, aiRetouchStatus: 0, aiRetouchStatusName: "已上传")
.badgePresentation,
TravelAlbumMaterial(isPurchased: true).purchaseBadgePresentation,
TravelAlbumMaterialBadgePresentation(kind: .purchased, text: "已购")
)
XCTAssertNil(TravelAlbumMaterial(isPurchased: true).aiRetouchBadgePresentation)
let purchasedAndProcessing = TravelAlbumMaterial(isPurchased: true, aiRetouchStatus: 2)
XCTAssertEqual(
purchasedAndProcessing.purchaseBadgePresentation,
TravelAlbumMaterialBadgePresentation(kind: .purchased, text: "已购")
)
XCTAssertEqual(
TravelAlbumMaterial(aiRetouchStatus: 1, aiRetouchStatusName: " 排队中 ").badgePresentation,
TravelAlbumMaterialBadgePresentation(kind: .pending, text: "排队中")
)
XCTAssertEqual(
TravelAlbumMaterial(aiRetouchStatus: 2, aiRetouchStatusName: " ").badgePresentation,
purchasedAndProcessing.aiRetouchBadgePresentation,
TravelAlbumMaterialBadgePresentation(kind: .processing, text: "修图中")
)
XCTAssertEqual(
TravelAlbumMaterial(aiRetouchStatus: 3).badgePresentation,
TravelAlbumMaterial(aiRetouchStatus: 1, aiRetouchStatusName: " 排队中 ")
.aiRetouchBadgePresentation,
TravelAlbumMaterialBadgePresentation(kind: .pending, text: "排队中")
)
XCTAssertEqual(
TravelAlbumMaterial(aiRetouchStatus: 2, aiRetouchStatusName: " ").aiRetouchBadgePresentation,
TravelAlbumMaterialBadgePresentation(kind: .processing, text: "修图中")
)
XCTAssertEqual(
TravelAlbumMaterial(aiRetouchStatus: 3).aiRetouchBadgePresentation,
TravelAlbumMaterialBadgePresentation(kind: .retouched, text: "AI已修")
)
XCTAssertEqual(
TravelAlbumMaterial(aiRetouchStatus: 3, aiRetouchStatusName: "AI封面").badgePresentation,
TravelAlbumMaterial(aiRetouchStatus: 3, aiRetouchStatusName: "AI封面")
.aiRetouchBadgePresentation,
TravelAlbumMaterialBadgePresentation(kind: .cover, text: "AI封面")
)
XCTAssertEqual(
TravelAlbumMaterial(aiRetouchStatus: 4).badgePresentation,
TravelAlbumMaterial(aiRetouchStatus: 4).aiRetouchBadgePresentation,
TravelAlbumMaterialBadgePresentation(kind: .failed, text: "失败")
)
XCTAssertNil(TravelAlbumMaterial(aiRetouchStatus: 99, aiRetouchStatusName: "未知").badgePresentation)
XCTAssertNil(
TravelAlbumMaterial(aiRetouchStatus: 99, aiRetouchStatusName: "未知")
.aiRetouchBadgePresentation
)
}
func testDisplayFormatters() {
+49 -5
View File
@@ -279,14 +279,22 @@ final class TravelAlbumDetailViewModelTests: XCTestCase {
XCTAssertEqual(TravelAlbumDisplayFormatter.creationTimeText(""), "--")
}
func testOnlyUnpurchasedMaterialCanBeSelected() {
func testAllMaterialsCanBeSelectedAndPurchasedTabSupportsSelectionMode() async {
let api = TravelAlbumMockAPI()
api.materialListResponses = [TravelAlbumListResponse(total: 1, list: [])]
let viewModel = TravelAlbumDetailViewModel(albumId: 2)
await viewModel.selectTab(.purchased, api: api)
viewModel.toggleSelectionMode()
viewModel.toggleMaterialSelection(TravelAlbumMaterial(id: 1, status: 2))
XCTAssertTrue(viewModel.selectedMaterialIds.isEmpty)
viewModel.toggleMaterialSelection(TravelAlbumMaterial(id: 1, isPurchased: true))
viewModel.toggleMaterialSelection(TravelAlbumMaterial(id: 2, isPurchased: false))
viewModel.toggleMaterialSelection(TravelAlbumMaterial(id: 1, status: 1))
XCTAssertEqual(viewModel.selectedMaterialIds, [1])
XCTAssertTrue(viewModel.isSelectionMode)
XCTAssertEqual(viewModel.selectedMaterialIds, [1, 2])
XCTAssertEqual(viewModel.selectedPurchasedMaterialCount, 1)
XCTAssertEqual(
viewModel.deleteSelectedConfirmationMessage,
"已选择 2 张素材,其中 1 张已购买。不会删除已购买的项目,只会删除 1 张未购买的项目。"
)
}
func testCompletingAIRetouchClearsSelectionModeAndSelectedMaterials() {
@@ -405,6 +413,42 @@ final class TravelAlbumDetailViewModelTests: XCTestCase {
XCTAssertFalse(viewModel.isSelectionMode)
XCTAssertTrue(viewModel.selectedMaterialIds.isEmpty)
}
func testDeleteSelectedMaterialsFiltersPurchasedItemsFromBatchRequest() async {
let api = TravelAlbumMockAPI()
api.materialListResponses = [
TravelAlbumListResponse(total: 0, list: []),
TravelAlbumListResponse(total: 0, list: []),
TravelAlbumListResponse(total: 0, list: []),
]
let viewModel = TravelAlbumDetailViewModel(albumId: 5)
viewModel.toggleSelectionMode()
viewModel.toggleMaterialSelection(TravelAlbumMaterial(id: 10, isPurchased: true))
viewModel.toggleMaterialSelection(TravelAlbumMaterial(id: 11, isPurchased: false))
viewModel.toggleMaterialSelection(TravelAlbumMaterial(id: 12, isPurchased: true))
await viewModel.deleteSelectedMaterials(api: api)
XCTAssertEqual(api.deletedMaterialIDBatches, [[11]])
XCTAssertFalse(viewModel.isSelectionMode)
XCTAssertTrue(viewModel.selectedMaterialIds.isEmpty)
}
func testDeleteSelectedMaterialsDoesNotRequestAPIWhenAllSelectedItemsArePurchased() async {
let api = TravelAlbumMockAPI()
let viewModel = TravelAlbumDetailViewModel(albumId: 5)
var message = ""
viewModel.onShowMessage = { message = $0 }
viewModel.toggleSelectionMode()
viewModel.toggleMaterialSelection(TravelAlbumMaterial(id: 10, isPurchased: true))
await viewModel.deleteSelectedMaterials(api: api)
XCTAssertTrue(api.deletedMaterialIDBatches.isEmpty)
XCTAssertEqual(message, "选中的素材均已购买,未删除任何项目")
XCTAssertTrue(viewModel.isSelectionMode)
XCTAssertEqual(viewModel.selectedMaterialIds, [10])
}
}
/// 有线传输 ViewModel 测试。