feat: refine AI retouch workflow

This commit is contained in:
2026-08-17 15:09:15 +08:00
parent 8e444f7434
commit 76aa7bb4aa
6 changed files with 106 additions and 29 deletions
@@ -25,7 +25,7 @@ final class TravelAlbumAIRetouchTemplateViewModelTests: XCTestCase {
XCTAssertTrue(viewModel.shouldShowInitialTips)
XCTAssertEqual(
viewModel.initialTipsText,
"Tips:氛围感修图为选填,可横向选择一种样式;选中后每张照片会额外生成1个独立结果,第一张照片仍另生成封面。"
"Tips:氛围感修图为选填;封面风格为必选,将使用第一张照片另生成封面。"
)
XCTAssertEqual(viewModel.visibleCategories, [.refined, .atmosphere, .cover])
XCTAssertEqual(viewModel.selectedRefinedTemplateId, 11)
@@ -44,6 +44,14 @@ final class TravelAlbumAIRetouchTemplateViewModelTests: XCTestCase {
)
await viewModel.loadTemplates(api: api)
XCTAssertEqual(
viewModel.initialTipsText,
"Tips:氛围感修图为选填,选中后每张照片会额外生成1个独立结果。"
)
viewModel.toggleTemplate(id: 11, category: .refined)
XCTAssertEqual(viewModel.selectedRefinedTemplateId, 11)
viewModel.toggleTemplate(id: 21, category: .atmosphere)
XCTAssertEqual(viewModel.selectedAtmosphereTemplateId, 21)
@@ -126,7 +134,10 @@ final class TravelAlbumAIRetouchTemplateViewModelTests: XCTestCase {
await all.loadTemplates(api: api)
XCTAssertEqual(all.visibleCategories, [.refined, .atmosphere])
XCTAssertEqual(all.selectedRefinedTemplateId, 11)
XCTAssertEqual(all.selectedAtmosphereTemplateId, 21)
XCTAssertNil(all.selectedAtmosphereTemplateId)
XCTAssertTrue(all.isOptional(.atmosphere))
XCTAssertEqual(all.requiredQuota, 1)
XCTAssertTrue(all.canSubmit)
await all.submit(api: api)
XCTAssertEqual(api.aiReretouchRequests, [
@@ -149,7 +160,7 @@ final class TravelAlbumAIRetouchTemplateViewModelTests: XCTestCase {
aiRetouchBatchId: 90,
type: .all,
refinedTemplateId: 11,
atmosphereTemplateId: 21
atmosphereTemplateId: nil
),
])
}
@@ -172,12 +183,13 @@ final class TravelAlbumAIRetouchTemplateViewModelTests: XCTestCase {
XCTAssertTrue(api.aiReretouchRequests.isEmpty)
}
func testMissingRequiredTemplateDisablesMatchingWorkflow() async {
func testMissingOptionalAtmosphereDoesNotDisableAllReretouchWorkflow() async {
let api = makeAPI()
api.aiRetouchTemplatesResponse = TravelAlbumAIRetouchTemplatesResponse(
refinedTemplates: [template(11, "清透")],
atmosphereTemplates: [],
coverTemplates: []
coverTemplates: [],
remainingQuota: 100
)
let viewModel = TravelAlbumAIRetouchTemplateViewModel(
scenicId: 18,
@@ -186,8 +198,42 @@ final class TravelAlbumAIRetouchTemplateViewModelTests: XCTestCase {
await viewModel.loadTemplates(api: api)
XCTAssertFalse(viewModel.canSubmit)
XCTAssertEqual(viewModel.validationMessage, "暂无可用的氛围感修图模板")
XCTAssertTrue(viewModel.canSubmit)
XCTAssertNil(viewModel.validationMessage)
XCTAssertEqual(viewModel.requiredQuota, 1)
}
func testMissingRequiredRefinedAndCoverTemplatesDisableMatchingWorkflows() async {
let refinedAPI = makeAPI()
refinedAPI.aiRetouchTemplatesResponse = TravelAlbumAIRetouchTemplatesResponse(
refinedTemplates: [],
atmosphereTemplates: [template(21, "暖阳")],
coverTemplates: [template(31, "杂志")],
remainingQuota: 100
)
let reretouch = TravelAlbumAIRetouchTemplateViewModel(
scenicId: 18,
workflow: .reretouch(materialId: 7, batchId: 70, type: .all)
)
await reretouch.loadTemplates(api: refinedAPI)
XCTAssertFalse(reretouch.canSubmit)
XCTAssertEqual(reretouch.validationMessage, "暂无可用的原图精修模板")
let coverAPI = makeAPI()
coverAPI.aiRetouchTemplatesResponse = TravelAlbumAIRetouchTemplatesResponse(
refinedTemplates: [template(11, "清透")],
atmosphereTemplates: [template(21, "暖阳")],
coverTemplates: [],
remainingQuota: 100
)
let batch = TravelAlbumAIRetouchTemplateViewModel(
albumId: 8,
scenicId: 18,
materialIds: [1, 2, 3, 4]
)
await batch.loadTemplates(api: coverAPI)
XCTAssertFalse(batch.canSubmit)
XCTAssertEqual(batch.validationMessage, "暂无可用的封面风格模板")
}
func testLoadFailureExposesRetryMessageAndKeepsSubmissionDisabled() async {
+18 -1
View File
@@ -243,6 +243,7 @@ final class TravelAlbumAPITests: XCTestCase {
jobSubmissionJSON(batchId: 51, albumId: 6),
jobSubmissionJSON(batchId: 52, albumId: 6),
jobSubmissionJSON(batchId: 53, albumId: 6),
jobSubmissionJSON(batchId: 54, albumId: 6),
])
let api = TravelAlbumAPI(client: APIClient(environment: .testing, session: session))
@@ -273,13 +274,22 @@ final class TravelAlbumAPITests: XCTestCase {
atmosphereTemplateId: 22
)
)
try await api.submitAIReretouch(
TravelAlbumAIReretouchRequest(
id: 14,
aiRetouchBatchId: 54,
type: .all,
refinedTemplateId: 21,
atmosphereTemplateId: nil
)
)
let bodies = try session.requests.map { request in
try JSONSerialization.jsonObject(with: XCTUnwrap(request.httpBody)) as? [String: Any]
}
XCTAssertEqual(session.requests.map { $0.url?.path }, Array(
repeating: "/api/yf-handset-app/photog/travel-album/ai-reretouch",
count: 3
count: 4
))
XCTAssertEqual(Set(bodies[0]?.keys.map { $0 } ?? []), ["id", "ai_retouch_batch_id", "type", "refined_template_id"])
XCTAssertEqual(Set(bodies[1]?.keys.map { $0 } ?? []), ["id", "ai_retouch_batch_id", "type", "atmosphere_template_id"])
@@ -290,9 +300,16 @@ final class TravelAlbumAPITests: XCTestCase {
"refined_template_id",
"atmosphere_template_id",
])
XCTAssertEqual(Set(bodies[3]?.keys.map { $0 } ?? []), [
"id",
"ai_retouch_batch_id",
"type",
"refined_template_id",
])
XCTAssertEqual(bodies[0]?["type"] as? Int, 1)
XCTAssertEqual(bodies[1]?["type"] as? Int, 2)
XCTAssertEqual(bodies[2]?["type"] as? Int, 3)
XCTAssertEqual(bodies[3]?["type"] as? Int, 3)
}
func testAIJobListBuildsCursorQueryAndDecodesUnknownStatusSafely() async throws {
@@ -763,7 +763,7 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase {
XCTAssertEqual(selectionCountLabel.text, "已选择 4 张照片")
XCTAssertEqual(
tipsLabel.text,
"Tips:氛围感修图为选填,可横向选择一种样式;选中后每张照片会额外生成1个独立结果,第一张照片仍另生成封面。"
"Tips:氛围感修图为选填;封面风格为必选,将使用第一张照片另生成封面。"
)
XCTAssertFalse(tipsContainer.isHidden)
XCTAssertEqual(tipsContainer.backgroundColor?.travelAlbumTestHexRGB, 0xF4F8FF)
@@ -806,10 +806,9 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase {
} as? UICollectionView
)
let accessibleLabels = controller.view.allAccessibilityLabels()
XCTAssertTrue(accessibleLabels.contains("原图精修"))
XCTAssertFalse(accessibleLabels.contains("原图精修,必选"))
XCTAssertTrue(accessibleLabels.contains("原图精修,必选"))
XCTAssertTrue(accessibleLabels.contains("氛围感修图,选填"))
XCTAssertTrue(accessibleLabels.contains("封面风格,赠送 · 不占额度"))
XCTAssertTrue(accessibleLabels.contains("封面风格,必选 · 赠送 · 不占额度"))
collectionView.scrollToItem(
at: IndexPath(item: 0, section: 4),
@@ -954,7 +953,7 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase {
XCTAssertEqual(comparisonButton.bounds.size, CGSize(width: 48, height: 48))
}
func testPreviewAIRetouchUsesSelectedRetouchedTabWorkflowAndShowsTaskAction() async throws {
func testPreviewAIRetouchUsesSelectedRetouchedTabWorkflowAndNotifiesSubmission() async throws {
UIView.setAnimationsEnabled(false)
defer { UIView.setAnimationsEnabled(true) }
let api = TravelAlbumMockAPI()
@@ -985,13 +984,15 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase {
),
]
)
var submissionNotified = false
let controller = TravelAlbumPhotoPreviewViewController(
projects: [project],
totalCount: 1,
startProjectIndex: 0,
albumId: 8,
scenicIdProvider: { 18 },
aiRetouchAPI: api
aiRetouchAPI: api,
onAIRetouchSubmitted: { submissionNotified = true }
)
let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 390, height: 844))
window.rootViewController = controller
@@ -1048,12 +1049,10 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase {
XCTAssertTrue(tipsLabel.isHidden)
confirmButton.sendActions(for: .touchUpInside)
await waitUntil { api.aiReretouchRequests.count == 1 }
await waitUntil { controller.presentedViewController is UIAlertController }
await waitUntil { submissionNotified }
XCTAssertEqual(api.aiReretouchRequests.first?.type, .refined)
let successAlert = try XCTUnwrap(controller.presentedViewController as? UIAlertController)
XCTAssertEqual(successAlert.title, "AI修图任务已提交")
XCTAssertEqual(successAlert.actions.map(\.title), ["知道了", "查看任务"])
XCTAssertTrue(submissionNotified)
XCTAssertTrue(window.rootViewController === controller)
}