feat: add AI retouch and album preview actions

This commit is contained in:
2026-08-10 16:00:32 +08:00
parent 24fa66281c
commit 439edf827c
14 changed files with 2110 additions and 118 deletions
+138
View File
@@ -26,6 +26,46 @@ final class TravelAlbumModelsTests: XCTestCase {
XCTAssertFalse(project.hasVariants)
}
func testPreviewProjectMapsNonemptyAIResultURLsInCanonicalOrder() {
let material = TravelAlbumMaterial(
id: 8,
fileName: "IMG_0008.JPG",
fileUrl: "https://cdn.example.com/original.jpg",
fileSize: 4096,
coverUrl: "https://cdn.example.com/cover.jpg",
aiRefinedURL: " https://cdn.example.com/refined.jpg ",
aiAtmosphereURL: "https://cdn.example.com/atmosphere.jpg"
)
let project = TravelAlbumPreviewProject(material: material)
XCTAssertEqual(project.orderedAssets.map(\.kind), [.original, .retouched, .atmosphere])
XCTAssertEqual(
project.asset(for: .retouched)?.displayURL,
"https://cdn.example.com/refined.jpg"
)
XCTAssertEqual(
project.asset(for: .atmosphere)?.previewURL,
"https://cdn.example.com/atmosphere.jpg"
)
XCTAssertNil(project.asset(for: .cover))
XCTAssertTrue(project.hasVariants)
}
func testPreviewProjectIgnoresBlankAIResultURLs() {
let material = TravelAlbumMaterial(
id: 8,
fileUrl: "https://cdn.example.com/original.jpg",
aiRefinedURL: " \n ",
aiAtmosphereURL: "\t"
)
XCTAssertEqual(
TravelAlbumPreviewProject(material: material).orderedAssets.map(\.kind),
[.original]
)
}
func testPreviewAssetFallsBackToAvailableURLForBothQualityLevels() {
let missingOriginal = TravelAlbumPreviewAsset(
id: "cover-only",
@@ -91,6 +131,36 @@ final class TravelAlbumModelsTests: XCTestCase {
XCTAssertEqual(nodes[target], TravelAlbumPreviewNode(projectIndex: 0, kind: .original))
}
func testPreviewDeletionPrefersNextThenFallsBackToPreviousAndClosesWhenEmpty() {
XCTAssertEqual(
TravelAlbumPreviewNavigator.projectIndexAfterDeletion(
deletedProjectIndex: 0,
remainingProjectCount: 2
),
0
)
XCTAssertEqual(
TravelAlbumPreviewNavigator.projectIndexAfterDeletion(
deletedProjectIndex: 1,
remainingProjectCount: 2
),
1
)
XCTAssertEqual(
TravelAlbumPreviewNavigator.projectIndexAfterDeletion(
deletedProjectIndex: 2,
remainingProjectCount: 2
),
1
)
XCTAssertNil(
TravelAlbumPreviewNavigator.projectIndexAfterDeletion(
deletedProjectIndex: 0,
remainingProjectCount: 0
)
)
}
func testPlaceholderPreviewActionsReturnUnavailableWithoutMutation() async {
let handler = PlaceholderTravelAlbumPreviewActionHandler()
let aiResult = await handler.requestAIRetouch(originalMaterialId: 9)
@@ -153,6 +223,10 @@ final class TravelAlbumModelsTests: XCTestCase {
"file_size": 2048,
"cover_url": "",
"is_purchased": false,
"ai_retouch_status": 3,
"ai_retouch_status_name": "AI已修",
"ai_refined_url": "https://cdn/refined.jpg",
"ai_atmosphere_url": "https://cdn/atmosphere.jpg",
"created_at": "",
"updated_at": ""
}
@@ -162,6 +236,70 @@ final class TravelAlbumModelsTests: XCTestCase {
XCTAssertEqual(material.userEquityTravelId, 7)
XCTAssertEqual(material.fileName, "IMG_0001.JPG")
XCTAssertFalse(material.isPurchased)
XCTAssertEqual(material.aiRetouchStatus, 3)
XCTAssertEqual(material.aiRetouchStatusName, "AI已修")
XCTAssertEqual(material.aiRefinedURL, "https://cdn/refined.jpg")
XCTAssertEqual(material.aiAtmosphereURL, "https://cdn/atmosphere.jpg")
}
func testTravelAlbumMaterialDefaultsInvalidAIFields() throws {
let json = """
{
"id": 11,
"user_equity_travel_id": 7,
"status": 1,
"order_number": "",
"user_id": 9,
"file_name": "IMG_0001.JPG",
"file_type": 2,
"file_url": "https://cdn/a.jpg",
"file_size": 2048,
"cover_url": "",
"is_purchased": false,
"ai_retouch_status": "invalid",
"ai_retouch_status_name": null,
"ai_refined_url": 123,
"created_at": "",
"updated_at": ""
}
""".data(using: .utf8)!
let material = try JSONDecoder().decode(TravelAlbumMaterial.self, from: json)
XCTAssertEqual(material.aiRetouchStatus, 0)
XCTAssertEqual(material.aiRetouchStatusName, "")
XCTAssertEqual(material.aiRefinedURL, "")
XCTAssertEqual(material.aiAtmosphereURL, "")
}
func testTravelAlbumMaterialBadgePriorityAndFallbacks() {
XCTAssertNil(TravelAlbumMaterial(aiRetouchStatus: 0).badgePresentation)
XCTAssertEqual(
TravelAlbumMaterial(isPurchased: true, aiRetouchStatus: 0, aiRetouchStatusName: "已上传")
.badgePresentation,
TravelAlbumMaterialBadgePresentation(kind: .purchased, text: "已购")
)
XCTAssertEqual(
TravelAlbumMaterial(aiRetouchStatus: 1, aiRetouchStatusName: " 排队中 ").badgePresentation,
TravelAlbumMaterialBadgePresentation(kind: .pending, text: "排队中")
)
XCTAssertEqual(
TravelAlbumMaterial(aiRetouchStatus: 2, aiRetouchStatusName: " ").badgePresentation,
TravelAlbumMaterialBadgePresentation(kind: .processing, text: "修图中")
)
XCTAssertEqual(
TravelAlbumMaterial(aiRetouchStatus: 3).badgePresentation,
TravelAlbumMaterialBadgePresentation(kind: .retouched, text: "AI已修")
)
XCTAssertEqual(
TravelAlbumMaterial(aiRetouchStatus: 3, aiRetouchStatusName: "AI封面").badgePresentation,
TravelAlbumMaterialBadgePresentation(kind: .cover, text: "AI封面")
)
XCTAssertEqual(
TravelAlbumMaterial(aiRetouchStatus: 4).badgePresentation,
TravelAlbumMaterialBadgePresentation(kind: .failed, text: "失败")
)
XCTAssertNil(TravelAlbumMaterial(aiRetouchStatus: 99, aiRetouchStatusName: "未知").badgePresentation)
}
func testDisplayFormatters() {