feat: 接通图片预览 AI 修图流程
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -153,7 +153,7 @@ final class TravelAlbumAPITests: XCTestCase {
|
||||
materialIds: [11, 12, 13, 14],
|
||||
refinedTemplateId: 21,
|
||||
atmosphereTemplateId: 22,
|
||||
aiPreviewTabs: 31
|
||||
coverTemplateId: 31
|
||||
)
|
||||
)
|
||||
|
||||
@@ -165,7 +165,14 @@ final class TravelAlbumAPITests: XCTestCase {
|
||||
XCTAssertEqual(body?["material_ids"] as? [Int], [11, 12, 13, 14])
|
||||
XCTAssertEqual(body?["refined_template_id"] as? Int, 21)
|
||||
XCTAssertEqual(body?["atmosphere_template_id"] as? Int, 22)
|
||||
XCTAssertEqual(body?["ai_preview_tabs"] as? Int, 31)
|
||||
XCTAssertEqual(body?["cover_template_id"] as? Int, 31)
|
||||
XCTAssertEqual(Set(body?.keys.map { $0 } ?? []), [
|
||||
"user_equity_travel_id",
|
||||
"material_ids",
|
||||
"refined_template_id",
|
||||
"atmosphere_template_id",
|
||||
"cover_template_id",
|
||||
])
|
||||
}
|
||||
|
||||
func testSubmitAIRetouchOmitsAllOptionalTemplatesWhenAbsent() async throws {
|
||||
@@ -178,13 +185,75 @@ final class TravelAlbumAPITests: XCTestCase {
|
||||
materialIds: [11],
|
||||
refinedTemplateId: 21,
|
||||
atmosphereTemplateId: nil,
|
||||
aiPreviewTabs: nil
|
||||
coverTemplateId: nil
|
||||
)
|
||||
)
|
||||
|
||||
let body = try JSONSerialization.jsonObject(with: try XCTUnwrap(session.requests.first?.httpBody)) as? [String: Any]
|
||||
XCTAssertNil(body?["atmosphere_template_id"])
|
||||
XCTAssertNil(body?["ai_preview_tabs"])
|
||||
XCTAssertNil(body?["cover_template_id"])
|
||||
XCTAssertEqual(Set(body?.keys.map { $0 } ?? []), [
|
||||
"user_equity_travel_id",
|
||||
"material_ids",
|
||||
"refined_template_id",
|
||||
])
|
||||
}
|
||||
|
||||
func testSubmitAIReretouchEncodesOnlyFieldsRequiredByEachType() async throws {
|
||||
let session = MockURLSession(responses: [
|
||||
envelopeJSON(#"{"accepted":true}"#),
|
||||
envelopeJSON(#"{"accepted":true}"#),
|
||||
envelopeJSON(#"{"accepted":true}"#),
|
||||
])
|
||||
let api = TravelAlbumAPI(client: APIClient(environment: .testing, session: session))
|
||||
|
||||
try await api.submitAIReretouch(
|
||||
TravelAlbumAIReretouchRequest(
|
||||
id: 11,
|
||||
aiRetouchBatchId: 51,
|
||||
type: .refined,
|
||||
refinedTemplateId: 21,
|
||||
atmosphereTemplateId: nil
|
||||
)
|
||||
)
|
||||
try await api.submitAIReretouch(
|
||||
TravelAlbumAIReretouchRequest(
|
||||
id: 12,
|
||||
aiRetouchBatchId: 52,
|
||||
type: .atmosphere,
|
||||
refinedTemplateId: nil,
|
||||
atmosphereTemplateId: 22
|
||||
)
|
||||
)
|
||||
try await api.submitAIReretouch(
|
||||
TravelAlbumAIReretouchRequest(
|
||||
id: 13,
|
||||
aiRetouchBatchId: 53,
|
||||
type: .all,
|
||||
refinedTemplateId: 21,
|
||||
atmosphereTemplateId: 22
|
||||
)
|
||||
)
|
||||
|
||||
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
|
||||
))
|
||||
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"])
|
||||
XCTAssertEqual(Set(bodies[2]?.keys.map { $0 } ?? []), [
|
||||
"id",
|
||||
"ai_retouch_batch_id",
|
||||
"type",
|
||||
"refined_template_id",
|
||||
"atmosphere_template_id",
|
||||
])
|
||||
XCTAssertEqual(bodies[0]?["type"] as? Int, 1)
|
||||
XCTAssertEqual(bodies[1]?["type"] as? Int, 2)
|
||||
XCTAssertEqual(bodies[2]?["type"] as? Int, 3)
|
||||
}
|
||||
|
||||
private func envelopeJSON(_ dataJSON: String) -> Data {
|
||||
|
||||
@@ -352,6 +352,141 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase {
|
||||
XCTAssertEqual(cell.accessibilityValue, "未选择")
|
||||
}
|
||||
|
||||
func testPreviewAIRetouchUsesSelectedRetouchedTabWorkflowAndStaysPresented() async throws {
|
||||
UIView.setAnimationsEnabled(false)
|
||||
defer { UIView.setAnimationsEnabled(true) }
|
||||
let api = TravelAlbumMockAPI()
|
||||
api.aiRetouchTemplatesResponse = TravelAlbumAIRetouchTemplatesResponse(
|
||||
refinedTemplates: [TravelAlbumAIRetouchTemplate(id: 11, name: "清透", previewURL: "")],
|
||||
atmosphereTemplates: [TravelAlbumAIRetouchTemplate(id: 21, name: "暖阳", previewURL: "")]
|
||||
)
|
||||
let project = TravelAlbumPreviewProject(
|
||||
originalMaterialId: 7,
|
||||
aiRetouchBatchId: 70,
|
||||
assets: [
|
||||
TravelAlbumPreviewAsset(
|
||||
id: "original-7",
|
||||
kind: .original,
|
||||
fileURL: "",
|
||||
coverURL: "",
|
||||
fileName: "原图.jpg",
|
||||
fileSize: 0
|
||||
),
|
||||
TravelAlbumPreviewAsset(
|
||||
id: "retouched-7",
|
||||
kind: .retouched,
|
||||
fileURL: "",
|
||||
coverURL: "",
|
||||
fileName: "精修后.jpg",
|
||||
fileSize: 0
|
||||
),
|
||||
]
|
||||
)
|
||||
let controller = TravelAlbumPhotoPreviewViewController(
|
||||
projects: [project],
|
||||
totalCount: 1,
|
||||
startProjectIndex: 0,
|
||||
albumId: 8,
|
||||
scenicIdProvider: { 18 },
|
||||
aiRetouchAPI: api
|
||||
)
|
||||
let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 390, height: 844))
|
||||
window.rootViewController = controller
|
||||
window.makeKeyAndVisible()
|
||||
defer { window.isHidden = true }
|
||||
controller.loadViewIfNeeded()
|
||||
controller.view.layoutIfNeeded()
|
||||
|
||||
let retouchedButton = try XCTUnwrap(
|
||||
controller.view.findSubview { ($0 as? UIButton)?.accessibilityLabel == "精修后" } as? UIButton
|
||||
)
|
||||
retouchedButton.sendActions(for: .touchUpInside)
|
||||
let aiButton = try XCTUnwrap(
|
||||
controller.view.findSubview { ($0 as? UIButton)?.accessibilityLabel == "AI修图" } as? UIButton
|
||||
)
|
||||
aiButton.sendActions(for: .touchUpInside)
|
||||
await waitUntil { controller.presentedViewController is TravelAlbumAIRetouchTemplateViewController }
|
||||
let sheet = try XCTUnwrap(controller.presentedViewController as? TravelAlbumAIRetouchTemplateViewController)
|
||||
sheet.loadViewIfNeeded()
|
||||
let confirmButton = try XCTUnwrap(
|
||||
sheet.view.findSubview {
|
||||
$0.accessibilityIdentifier == "travelAlbum.aiRetouchConfirmButton"
|
||||
} as? UIButton
|
||||
)
|
||||
await waitUntil { api.aiRetouchTemplateScenicIds.count == 1 && confirmButton.isEnabled }
|
||||
sheet.view.frame = CGRect(x: 0, y: 0, width: 390, height: 844)
|
||||
sheet.view.layoutIfNeeded()
|
||||
|
||||
let labels = sheet.view.allAccessibilityLabels()
|
||||
XCTAssertTrue(labels.contains("原图精修"))
|
||||
XCTAssertFalse(labels.contains { $0.contains("氛围感修图") })
|
||||
confirmButton.sendActions(for: .touchUpInside)
|
||||
await waitUntil { api.aiReretouchRequests.count == 1 }
|
||||
await waitUntil { controller.presentedViewController == nil }
|
||||
|
||||
XCTAssertEqual(api.aiReretouchRequests.first?.type, .refined)
|
||||
XCTAssertNil(controller.presentedViewController)
|
||||
XCTAssertTrue(window.rootViewController === controller)
|
||||
}
|
||||
|
||||
func testPreviewRefreshPreservesVariantAndFallsBackWhenVariantDisappears() async throws {
|
||||
let original = TravelAlbumPreviewAsset(
|
||||
id: "original-7",
|
||||
kind: .original,
|
||||
fileURL: "",
|
||||
coverURL: "",
|
||||
fileName: "原图.jpg",
|
||||
fileSize: 0
|
||||
)
|
||||
let retouched = TravelAlbumPreviewAsset(
|
||||
id: "retouched-7",
|
||||
kind: .retouched,
|
||||
fileURL: "",
|
||||
coverURL: "",
|
||||
fileName: "精修后.jpg",
|
||||
fileSize: 0
|
||||
)
|
||||
var reloadCount = 0
|
||||
let controller = TravelAlbumPhotoPreviewViewController(
|
||||
projects: [TravelAlbumPreviewProject(originalMaterialId: 7, assets: [original, retouched])],
|
||||
totalCount: 1,
|
||||
startProjectIndex: 0,
|
||||
reload: {
|
||||
reloadCount += 1
|
||||
let assets = reloadCount == 1 ? [original, retouched] : [original]
|
||||
return ([TravelAlbumPreviewProject(originalMaterialId: 7, assets: assets)], 1)
|
||||
}
|
||||
)
|
||||
controller.loadViewIfNeeded()
|
||||
controller.view.frame = CGRect(x: 0, y: 0, width: 390, height: 844)
|
||||
controller.view.layoutIfNeeded()
|
||||
let retouchedButton = try XCTUnwrap(
|
||||
controller.view.findSubview { ($0 as? UIButton)?.accessibilityLabel == "精修后" } as? UIButton
|
||||
)
|
||||
retouchedButton.sendActions(for: .touchUpInside)
|
||||
let refreshButton = try XCTUnwrap(
|
||||
controller.view.findSubview {
|
||||
$0.accessibilityIdentifier == "travelAlbum.previewRefreshButton"
|
||||
} as? UIButton
|
||||
)
|
||||
|
||||
refreshButton.sendActions(for: .touchUpInside)
|
||||
await waitUntil { reloadCount == 1 && refreshButton.isEnabled }
|
||||
XCTAssertTrue(
|
||||
controller.view.findSubview {
|
||||
($0 as? UIButton)?.accessibilityLabel == "精修后"
|
||||
&& $0.accessibilityTraits.contains(.selected)
|
||||
} != nil
|
||||
)
|
||||
|
||||
refreshButton.sendActions(for: .touchUpInside)
|
||||
await waitUntil { reloadCount == 2 && refreshButton.isEnabled }
|
||||
XCTAssertNil(
|
||||
controller.view.findSubview { ($0 as? UIButton)?.accessibilityLabel == "精修后" }
|
||||
)
|
||||
XCTAssertTrue(controller.view.allLabels().contains { $0.text == "原图.jpg" })
|
||||
}
|
||||
|
||||
func testMaterialCellShowsSemanticStatusBadgeWithoutOverlappingSelectionCheck() throws {
|
||||
let cases: [(TravelAlbumMaterial, String, UInt)] = [
|
||||
(TravelAlbumMaterial(isPurchased: true), "已购", 0x475569),
|
||||
|
||||
@@ -33,6 +33,7 @@ final class TravelAlbumModelsTests: XCTestCase {
|
||||
fileUrl: "https://cdn.example.com/original.jpg",
|
||||
fileSize: 4096,
|
||||
coverUrl: "https://cdn.example.com/cover.jpg",
|
||||
aiRetouchBatchId: 55,
|
||||
aiRefinedURL: " https://cdn.example.com/refined.jpg ",
|
||||
aiAtmosphereURL: "https://cdn.example.com/atmosphere.jpg"
|
||||
)
|
||||
@@ -50,6 +51,7 @@ final class TravelAlbumModelsTests: XCTestCase {
|
||||
)
|
||||
XCTAssertNil(project.asset(for: .cover))
|
||||
XCTAssertTrue(project.hasVariants)
|
||||
XCTAssertEqual(project.aiRetouchBatchId, 55)
|
||||
}
|
||||
|
||||
func testPreviewProjectIgnoresBlankAIResultURLs() {
|
||||
@@ -161,24 +163,40 @@ final class TravelAlbumModelsTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testPlaceholderPreviewActionsReturnUnavailableWithoutMutation() async {
|
||||
let handler = PlaceholderTravelAlbumPreviewActionHandler()
|
||||
let aiResult = await handler.requestAIRetouch(originalMaterialId: 9)
|
||||
let deleteResult = await handler.deleteProject(originalMaterialId: 9)
|
||||
let refreshResult = await handler.refreshProject(originalMaterialId: 9)
|
||||
func testPreviewProjectMapsCurrentTabToMinimalAIRetouchWorkflow() {
|
||||
let originalOnly = makePreviewProject(id: 9, kinds: [.original])
|
||||
let variants = TravelAlbumPreviewProject(
|
||||
originalMaterialId: 10,
|
||||
aiRetouchBatchId: 88,
|
||||
assets: makePreviewProject(id: 10, kinds: [.original, .retouched, .atmosphere]).assets
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
aiResult,
|
||||
.unavailable("AI修图功能待接口接入")
|
||||
originalOnly.aiRetouchWorkflow(albumId: 7, selectedKind: .original),
|
||||
.initial(albumId: 7, materialIds: [9])
|
||||
)
|
||||
XCTAssertEqual(
|
||||
variants.aiRetouchWorkflow(albumId: 7, selectedKind: .original),
|
||||
.reretouch(materialId: 10, batchId: 88, type: .all)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
variants.aiRetouchWorkflow(albumId: 7, selectedKind: .retouched),
|
||||
.reretouch(materialId: 10, batchId: 88, type: .refined)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
variants.aiRetouchWorkflow(albumId: 7, selectedKind: .atmosphere),
|
||||
.reretouch(materialId: 10, batchId: 88, type: .atmosphere)
|
||||
)
|
||||
XCTAssertNil(variants.aiRetouchWorkflow(albumId: 7, selectedKind: .cover))
|
||||
}
|
||||
|
||||
func testPlaceholderPreviewDeleteReturnsUnavailableWithoutMutation() async {
|
||||
let handler = PlaceholderTravelAlbumPreviewActionHandler()
|
||||
let deleteResult = await handler.deleteProject(originalMaterialId: 9)
|
||||
XCTAssertEqual(
|
||||
deleteResult,
|
||||
.unavailable("项目删除接口待接入")
|
||||
)
|
||||
XCTAssertEqual(
|
||||
refreshResult,
|
||||
.unavailable("关联图片刷新接口待接入")
|
||||
)
|
||||
}
|
||||
|
||||
func testTravelAlbumDecodesSnakeCaseFields() throws {
|
||||
@@ -225,6 +243,7 @@ final class TravelAlbumModelsTests: XCTestCase {
|
||||
"is_purchased": false,
|
||||
"ai_retouch_status": 3,
|
||||
"ai_retouch_status_name": "AI已修",
|
||||
"ai_retouch_batch_id": 66,
|
||||
"ai_refined_url": "https://cdn/refined.jpg",
|
||||
"ai_atmosphere_url": "https://cdn/atmosphere.jpg",
|
||||
"created_at": "",
|
||||
@@ -238,6 +257,7 @@ final class TravelAlbumModelsTests: XCTestCase {
|
||||
XCTAssertFalse(material.isPurchased)
|
||||
XCTAssertEqual(material.aiRetouchStatus, 3)
|
||||
XCTAssertEqual(material.aiRetouchStatusName, "AI已修")
|
||||
XCTAssertEqual(material.aiRetouchBatchId, 66)
|
||||
XCTAssertEqual(material.aiRefinedURL, "https://cdn/refined.jpg")
|
||||
XCTAssertEqual(material.aiAtmosphereURL, "https://cdn/atmosphere.jpg")
|
||||
}
|
||||
@@ -258,6 +278,7 @@ final class TravelAlbumModelsTests: XCTestCase {
|
||||
"is_purchased": false,
|
||||
"ai_retouch_status": "invalid",
|
||||
"ai_retouch_status_name": null,
|
||||
"ai_retouch_batch_id": "invalid",
|
||||
"ai_refined_url": 123,
|
||||
"created_at": "",
|
||||
"updated_at": ""
|
||||
@@ -268,6 +289,7 @@ final class TravelAlbumModelsTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(material.aiRetouchStatus, 0)
|
||||
XCTAssertEqual(material.aiRetouchStatusName, "")
|
||||
XCTAssertEqual(material.aiRetouchBatchId, 0)
|
||||
XCTAssertEqual(material.aiRefinedURL, "")
|
||||
XCTAssertEqual(material.aiAtmosphereURL, "")
|
||||
}
|
||||
|
||||
@@ -206,6 +206,41 @@ final class TravelAlbumDetailViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(viewModel.purchasedPhotoCount, 0)
|
||||
}
|
||||
|
||||
func testPreviewReloadRefetchesAllLoadedPagesAndReplacesMaterialsById() async throws {
|
||||
let api = TravelAlbumMockAPI()
|
||||
api.materialListResponses = [
|
||||
TravelAlbumListResponse(
|
||||
total: 35,
|
||||
list: (1 ... 30).map { TravelAlbumMaterial(id: $0) }
|
||||
),
|
||||
TravelAlbumListResponse(
|
||||
total: 35,
|
||||
list: (31 ... 35).map { TravelAlbumMaterial(id: $0) }
|
||||
),
|
||||
TravelAlbumListResponse(
|
||||
total: 35,
|
||||
list: (1 ... 30).map {
|
||||
TravelAlbumMaterial(id: $0, aiRetouchBatchId: $0 == 7 ? 700 : 0)
|
||||
}
|
||||
),
|
||||
TravelAlbumListResponse(
|
||||
total: 35,
|
||||
list: (31 ... 35).map { TravelAlbumMaterial(id: $0) }
|
||||
),
|
||||
]
|
||||
let viewModel = TravelAlbumDetailViewModel(albumId: 2)
|
||||
await viewModel.loadMaterials(reset: true, api: api)
|
||||
await viewModel.loadMaterials(reset: false, api: api)
|
||||
|
||||
let response = try await viewModel.reloadLoadedMaterials(api: api)
|
||||
|
||||
XCTAssertEqual(response.total, 35)
|
||||
XCTAssertEqual(response.list.count, 35)
|
||||
XCTAssertEqual(response.list.first { $0.id == 7 }?.aiRetouchBatchId, 700)
|
||||
XCTAssertEqual(api.materialRequests.suffix(2).map(\.page), [1, 2])
|
||||
XCTAssertEqual(api.materialRequests.suffix(2).map(\.pageSize), [30, 30])
|
||||
}
|
||||
|
||||
func testDeleteAlbumCallsCallback() async {
|
||||
let api = TravelAlbumMockAPI()
|
||||
let viewModel = TravelAlbumDetailViewModel(albumId: 5)
|
||||
@@ -1053,6 +1088,8 @@ final class TravelAlbumMockAPI: TravelAlbumServing {
|
||||
var aiRetouchTemplatesError: Error?
|
||||
var submitAIRetouchError: Error?
|
||||
var submitAIRetouchDelayNanoseconds: UInt64 = 0
|
||||
var submitAIReretouchError: Error?
|
||||
var submitAIReretouchDelayNanoseconds: UInt64 = 0
|
||||
var deleteMaterialError: Error?
|
||||
var deleteMaterialDelayNanoseconds: UInt64 = 0
|
||||
|
||||
@@ -1065,6 +1102,7 @@ final class TravelAlbumMockAPI: TravelAlbumServing {
|
||||
private(set) var deletedMaterialIds: [Int] = []
|
||||
private(set) var aiRetouchTemplateScenicIds: [Int] = []
|
||||
private(set) var aiRetouchRequests: [TravelAlbumAIRetouchRequest] = []
|
||||
private(set) var aiReretouchRequests: [TravelAlbumAIReretouchRequest] = []
|
||||
|
||||
func availableOrders() async throws -> [TravelAlbumAvailableOrder] {
|
||||
availableOrdersCallCount += 1
|
||||
@@ -1148,4 +1186,12 @@ final class TravelAlbumMockAPI: TravelAlbumServing {
|
||||
}
|
||||
if let submitAIRetouchError { throw submitAIRetouchError }
|
||||
}
|
||||
|
||||
func submitAIReretouch(_ request: TravelAlbumAIReretouchRequest) async throws {
|
||||
aiReretouchRequests.append(request)
|
||||
if submitAIReretouchDelayNanoseconds > 0 {
|
||||
try await Task.sleep(nanoseconds: submitAIReretouchDelayNanoseconds)
|
||||
}
|
||||
if let submitAIReretouchError { throw submitAIReretouchError }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user