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
@@ -0,0 +1,191 @@
//
// TravelAlbumAIRetouchTemplateViewModelTests.swift
// suixinkanTests
//
import XCTest
@testable import suixinkan
/// AI 修图模板选择状态测试。
@MainActor
final class TravelAlbumAIRetouchTemplateViewModelTests: XCTestCase {
func testLoadDefaultsRequiredSelectionsAndLeavesAtmosphereEmpty() async {
let api = makeAPI()
let viewModel = TravelAlbumAIRetouchTemplateViewModel(
albumId: 8,
scenicId: 18,
materialIds: [4, 3, 2, 1]
)
await viewModel.loadTemplates(api: api)
XCTAssertEqual(api.aiRetouchTemplateScenicIds, [18])
XCTAssertEqual(viewModel.selectedRefinedTemplateId, 11)
XCTAssertNil(viewModel.selectedAtmosphereTemplateId)
XCTAssertEqual(viewModel.selectedCoverTemplateId, 31)
XCTAssertTrue(viewModel.showsCoverTemplates)
XCTAssertTrue(viewModel.canSubmit)
}
func testAtmosphereSelectionTogglesOffWhenTappedAgain() async {
let api = makeAPI()
let viewModel = TravelAlbumAIRetouchTemplateViewModel(
albumId: 8,
scenicId: 18,
materialIds: [1]
)
await viewModel.loadTemplates(api: api)
viewModel.toggleTemplate(id: 21, category: .atmosphere)
XCTAssertEqual(viewModel.selectedAtmosphereTemplateId, 21)
viewModel.toggleTemplate(id: 21, category: .atmosphere)
XCTAssertNil(viewModel.selectedAtmosphereTemplateId)
}
func testThreeMaterialsHideCoverAndOmitCoverFromSubmission() async {
let api = makeAPI()
let viewModel = TravelAlbumAIRetouchTemplateViewModel(
albumId: 8,
scenicId: 18,
materialIds: [3, 1, 2]
)
var submitted = false
viewModel.onSubmitted = { submitted = true }
await viewModel.loadTemplates(api: api)
XCTAssertFalse(viewModel.showsCoverTemplates)
XCTAssertNil(viewModel.selectedCoverTemplateId)
await viewModel.submit(api: api)
XCTAssertTrue(submitted)
XCTAssertEqual(api.aiRetouchRequests.count, 1)
XCTAssertEqual(api.aiRetouchRequests.first?.materialIds, [1, 2, 3])
XCTAssertNil(api.aiRetouchRequests.first?.aiPreviewTabs)
}
func testFourMaterialsSubmitAllSelectedTemplateIds() async {
let api = makeAPI()
let viewModel = TravelAlbumAIRetouchTemplateViewModel(
albumId: 8,
scenicId: 18,
materialIds: [1, 2, 3, 4]
)
await viewModel.loadTemplates(api: api)
viewModel.toggleTemplate(id: 12, category: .refined)
viewModel.toggleTemplate(id: 21, category: .atmosphere)
await viewModel.submit(api: api)
XCTAssertEqual(
api.aiRetouchRequests.first,
TravelAlbumAIRetouchRequest(
userEquityTravelId: 8,
materialIds: [1, 2, 3, 4],
refinedTemplateId: 12,
atmosphereTemplateId: 21,
aiPreviewTabs: 31
)
)
}
func testMissingRequiredCoverTemplateDisablesSubmission() async {
let api = makeAPI()
api.aiRetouchTemplatesResponse = TravelAlbumAIRetouchTemplatesResponse(
refinedTemplates: [template(11, "清透")],
atmosphereTemplates: [template(21, "暖阳")],
coverTemplates: []
)
let viewModel = TravelAlbumAIRetouchTemplateViewModel(
albumId: 8,
scenicId: 18,
materialIds: [1, 2, 3, 4]
)
await viewModel.loadTemplates(api: api)
XCTAssertFalse(viewModel.canSubmit)
XCTAssertEqual(viewModel.validationMessage, "暂无可用的封面风格模板")
}
func testLoadFailureExposesRetryMessageAndKeepsSubmissionDisabled() async {
let api = makeAPI()
api.aiRetouchTemplatesError = APIError.serverCode(500, "模板服务繁忙")
let viewModel = TravelAlbumAIRetouchTemplateViewModel(
albumId: 8,
scenicId: 18,
materialIds: [1]
)
await viewModel.loadTemplates(api: api)
XCTAssertEqual(viewModel.loadErrorMessage, "模板服务繁忙")
XCTAssertFalse(viewModel.canSubmit)
XCTAssertTrue(viewModel.refinedTemplates.isEmpty)
}
func testSubmitFailureKeepsSelectionAndAllowsRetry() async {
let api = makeAPI()
api.submitAIRetouchError = APIError.serverCode(500, "提交服务繁忙")
let viewModel = TravelAlbumAIRetouchTemplateViewModel(
albumId: 8,
scenicId: 18,
materialIds: [1]
)
var message: String?
var submitted = false
viewModel.onShowMessage = { message = $0 }
viewModel.onSubmitted = { submitted = true }
await viewModel.loadTemplates(api: api)
await viewModel.submit(api: api)
XCTAssertEqual(api.aiRetouchRequests.count, 1)
XCTAssertEqual(message, "提交服务繁忙")
XCTAssertFalse(submitted)
XCTAssertFalse(viewModel.isSubmitting)
XCTAssertTrue(viewModel.canSubmit)
XCTAssertEqual(viewModel.selectedRefinedTemplateId, 11)
}
func testSubmittingPreventsDuplicateRequest() async {
let api = makeAPI()
api.submitAIRetouchDelayNanoseconds = 50_000_000
let viewModel = TravelAlbumAIRetouchTemplateViewModel(
albumId: 8,
scenicId: 18,
materialIds: [1]
)
await viewModel.loadTemplates(api: api)
let firstSubmission = Task { await viewModel.submit(api: api) }
await waitUntil { viewModel.isSubmitting }
await viewModel.submit(api: api)
await firstSubmission.value
XCTAssertEqual(api.aiRetouchRequests.count, 1)
XCTAssertFalse(viewModel.isSubmitting)
}
private func makeAPI() -> TravelAlbumMockAPI {
let api = TravelAlbumMockAPI()
api.aiRetouchTemplatesResponse = TravelAlbumAIRetouchTemplatesResponse(
refinedTemplates: [template(11, "清透"), template(12, "自然")],
atmosphereTemplates: [template(21, "暖阳")],
coverTemplates: [template(31, "杂志")]
)
return api
}
private func template(_ id: Int, _ name: String) -> TravelAlbumAIRetouchTemplate {
TravelAlbumAIRetouchTemplate(id: id, name: name, previewURL: "https://cdn.example.com/\(id).jpg")
}
private func waitUntil(_ condition: @escaping () -> Bool) async {
for _ in 0 ..< 100 {
if condition() { return }
await Task.yield()
}
}
}