feat: 接通图片预览 AI 修图流程

This commit is contained in:
2026-08-11 09:52:15 +08:00
parent 5704531f3a
commit 43c75c8e36
14 changed files with 814 additions and 169 deletions
@@ -6,28 +6,30 @@
import XCTest
@testable import suixinkan
/// AI 修图模板选择状态测试。
/// AI 修图模板工作流、选择状态与请求分流测试。
@MainActor
final class TravelAlbumAIRetouchTemplateViewModelTests: XCTestCase {
func testLoadDefaultsRequiredSelectionsAndLeavesAtmosphereEmpty() async {
func testInitialWorkflowDefaultsRequiredSelectionsAndLeavesAtmosphereEmpty() async {
let api = makeAPI()
let viewModel = TravelAlbumAIRetouchTemplateViewModel(
albumId: 8,
scenicId: 18,
materialIds: [4, 3, 2, 1]
materialIds: [4, 3, 2, 1, 1]
)
await viewModel.loadTemplates(api: api)
XCTAssertEqual(api.aiRetouchTemplateScenicIds, [18])
XCTAssertEqual(viewModel.workflow, .initial(albumId: 8, materialIds: [1, 2, 3, 4]))
XCTAssertEqual(viewModel.visibleCategories, [.refined, .atmosphere, .cover])
XCTAssertEqual(viewModel.selectedRefinedTemplateId, 11)
XCTAssertNil(viewModel.selectedAtmosphereTemplateId)
XCTAssertEqual(viewModel.selectedCoverTemplateId, 31)
XCTAssertTrue(viewModel.showsCoverTemplates)
XCTAssertTrue(viewModel.isOptional(.atmosphere))
XCTAssertTrue(viewModel.canSubmit)
}
func testAtmosphereSelectionTogglesOffWhenTappedAgain() async {
func testInitialAtmosphereSelectionTogglesOffWhenTappedAgain() async {
let api = makeAPI()
let viewModel = TravelAlbumAIRetouchTemplateViewModel(
albumId: 8,
@@ -43,70 +45,141 @@ final class TravelAlbumAIRetouchTemplateViewModelTests: XCTestCase {
XCTAssertNil(viewModel.selectedAtmosphereTemplateId)
}
func testThreeMaterialsHideCoverAndOmitCoverFromSubmission() async {
func testInitialSubmissionUsesCoverTemplateOnlyForFourOrMoreMaterials() async {
let api = makeAPI()
let viewModel = TravelAlbumAIRetouchTemplateViewModel(
let threePhotoViewModel = TravelAlbumAIRetouchTemplateViewModel(
albumId: 8,
scenicId: 18,
materialIds: [3, 1, 2]
)
var submitted = false
viewModel.onSubmitted = { submitted = true }
await viewModel.loadTemplates(api: api)
await threePhotoViewModel.loadTemplates(api: api)
await threePhotoViewModel.submit(api: api)
XCTAssertFalse(viewModel.showsCoverTemplates)
XCTAssertNil(viewModel.selectedCoverTemplateId)
XCTAssertEqual(
api.aiRetouchRequests[0],
TravelAlbumAIRetouchRequest(
userEquityTravelId: 8,
materialIds: [1, 2, 3],
refinedTemplateId: 11,
atmosphereTemplateId: nil,
coverTemplateId: nil
)
)
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(
let fourPhotoViewModel = 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)
await fourPhotoViewModel.loadTemplates(api: api)
fourPhotoViewModel.toggleTemplate(id: 12, category: .refined)
fourPhotoViewModel.toggleTemplate(id: 21, category: .atmosphere)
await fourPhotoViewModel.submit(api: api)
XCTAssertEqual(
api.aiRetouchRequests.first,
api.aiRetouchRequests[1],
TravelAlbumAIRetouchRequest(
userEquityTravelId: 8,
materialIds: [1, 2, 3, 4],
refinedTemplateId: 12,
atmosphereTemplateId: 21,
aiPreviewTabs: 31
coverTemplateId: 31
)
)
}
func testMissingRequiredCoverTemplateDisablesSubmission() async {
func testReretouchWorkflowShowsAndSubmitsOnlyRequiredCategories() async {
let api = makeAPI()
let refined = TravelAlbumAIRetouchTemplateViewModel(
scenicId: 18,
workflow: .reretouch(materialId: 7, batchId: 70, type: .refined)
)
await refined.loadTemplates(api: api)
XCTAssertEqual(refined.visibleCategories, [.refined])
XCTAssertEqual(refined.selectedRefinedTemplateId, 11)
XCTAssertNil(refined.selectedAtmosphereTemplateId)
await refined.submit(api: api)
let atmosphere = TravelAlbumAIRetouchTemplateViewModel(
scenicId: 18,
workflow: .reretouch(materialId: 8, batchId: 80, type: .atmosphere)
)
await atmosphere.loadTemplates(api: api)
XCTAssertEqual(atmosphere.visibleCategories, [.atmosphere])
XCTAssertEqual(atmosphere.selectedAtmosphereTemplateId, 21)
atmosphere.toggleTemplate(id: 21, category: .atmosphere)
XCTAssertEqual(atmosphere.selectedAtmosphereTemplateId, 21)
await atmosphere.submit(api: api)
let all = TravelAlbumAIRetouchTemplateViewModel(
scenicId: 18,
workflow: .reretouch(materialId: 9, batchId: 90, type: .all)
)
await all.loadTemplates(api: api)
XCTAssertEqual(all.visibleCategories, [.refined, .atmosphere])
XCTAssertEqual(all.selectedRefinedTemplateId, 11)
XCTAssertEqual(all.selectedAtmosphereTemplateId, 21)
await all.submit(api: api)
XCTAssertEqual(api.aiReretouchRequests, [
TravelAlbumAIReretouchRequest(
id: 7,
aiRetouchBatchId: 70,
type: .refined,
refinedTemplateId: 11,
atmosphereTemplateId: nil
),
TravelAlbumAIReretouchRequest(
id: 8,
aiRetouchBatchId: 80,
type: .atmosphere,
refinedTemplateId: nil,
atmosphereTemplateId: 21
),
TravelAlbumAIReretouchRequest(
id: 9,
aiRetouchBatchId: 90,
type: .all,
refinedTemplateId: 11,
atmosphereTemplateId: 21
),
])
}
func testInvalidReretouchBatchDisablesSubmission() async {
let api = makeAPI()
let viewModel = TravelAlbumAIRetouchTemplateViewModel(
scenicId: 18,
workflow: .reretouch(materialId: 7, batchId: 0, type: .refined)
)
var message: String?
viewModel.onShowMessage = { message = $0 }
await viewModel.loadTemplates(api: api)
await viewModel.submit(api: api)
XCTAssertFalse(viewModel.canSubmit)
XCTAssertEqual(viewModel.validationMessage, "当前图片缺少修图批次,请刷新后重试")
XCTAssertEqual(message, "当前图片缺少修图批次,请刷新后重试")
XCTAssertTrue(api.aiReretouchRequests.isEmpty)
}
func testMissingRequiredTemplateDisablesMatchingWorkflow() async {
let api = makeAPI()
api.aiRetouchTemplatesResponse = TravelAlbumAIRetouchTemplatesResponse(
refinedTemplates: [template(11, "清透")],
atmosphereTemplates: [template(21, "暖阳")],
atmosphereTemplates: [],
coverTemplates: []
)
let viewModel = TravelAlbumAIRetouchTemplateViewModel(
albumId: 8,
scenicId: 18,
materialIds: [1, 2, 3, 4]
workflow: .reretouch(materialId: 7, batchId: 70, type: .all)
)
await viewModel.loadTemplates(api: api)
XCTAssertFalse(viewModel.canSubmit)
XCTAssertEqual(viewModel.validationMessage, "暂无可用的封面风格模板")
XCTAssertEqual(viewModel.validationMessage, "暂无可用的氛围感修图模板")
}
func testLoadFailureExposesRetryMessageAndKeepsSubmissionDisabled() async {
@@ -127,11 +200,10 @@ final class TravelAlbumAIRetouchTemplateViewModelTests: XCTestCase {
func testSubmitFailureKeepsSelectionAndAllowsRetry() async {
let api = makeAPI()
api.submitAIRetouchError = APIError.serverCode(500, "提交服务繁忙")
api.submitAIReretouchError = APIError.serverCode(500, "提交服务繁忙")
let viewModel = TravelAlbumAIRetouchTemplateViewModel(
albumId: 8,
scenicId: 18,
materialIds: [1]
workflow: .reretouch(materialId: 7, batchId: 70, type: .refined)
)
var message: String?
var submitted = false
@@ -141,7 +213,7 @@ final class TravelAlbumAIRetouchTemplateViewModelTests: XCTestCase {
await viewModel.submit(api: api)
XCTAssertEqual(api.aiRetouchRequests.count, 1)
XCTAssertEqual(api.aiReretouchRequests.count, 1)
XCTAssertEqual(message, "提交服务繁忙")
XCTAssertFalse(submitted)
XCTAssertFalse(viewModel.isSubmitting)