feat: add AI retouch before-after comparison
This commit is contained in:
@@ -162,7 +162,7 @@ final class TravelAlbumAPITests: XCTestCase {
|
||||
|
||||
func testAIRetouchTemplatesBuildsQueryAndDecodesGroups() async throws {
|
||||
let data = envelopeJSON(
|
||||
#"{"refined_templates":[{"id":1,"name":"清透","preview_url":"https://cdn/refined.jpg"}],"atmosphere_templates":[{"id":2,"name":"暖阳","preview_url":"https://cdn/atmosphere.jpg"}],"cover_templates":[{"id":3,"name":"杂志","preview_url":"https://cdn/cover.jpg"}],"remaining_quota":12}"#
|
||||
#"{"refined_templates":[{"id":1,"name":"清透","preview_url":"https://cdn/refined.jpg","before_url":"https://cdn/refined-before.jpg","after_url":"https://cdn/refined-after.jpg"}],"atmosphere_templates":[{"id":2,"name":"暖阳","preview_url":"https://cdn/atmosphere.jpg","before_url":"https://cdn/atmosphere-before.jpg","after_url":"https://cdn/atmosphere-after.jpg"}],"cover_templates":[{"id":3,"name":"杂志","preview_url":"https://cdn/cover.jpg","before_url":"https://cdn/cover-before.jpg","after_url":"https://cdn/cover-after.jpg"}],"remaining_quota":12}"#
|
||||
)
|
||||
let session = MockURLSession(responses: [data])
|
||||
let api = TravelAlbumAPI(client: APIClient(environment: .testing, session: session))
|
||||
@@ -170,6 +170,8 @@ final class TravelAlbumAPITests: XCTestCase {
|
||||
let response = try await api.aiRetouchTemplates(scenicId: 18)
|
||||
|
||||
XCTAssertEqual(response.refinedTemplates.first?.name, "清透")
|
||||
XCTAssertEqual(response.refinedTemplates.first?.beforeURL, "https://cdn/refined-before.jpg")
|
||||
XCTAssertEqual(response.refinedTemplates.first?.afterURL, "https://cdn/refined-after.jpg")
|
||||
XCTAssertEqual(response.atmosphereTemplates.first?.id, 2)
|
||||
XCTAssertEqual(response.coverTemplates.first?.previewURL, "https://cdn/cover.jpg")
|
||||
XCTAssertEqual(response.remainingQuota, 12)
|
||||
|
||||
@@ -637,35 +637,96 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase {
|
||||
XCTAssertEqual(cell.accessibilityValue, "未选择")
|
||||
}
|
||||
|
||||
func testAIRetouchTemplatePreviewUsesImmersiveZoomableLayout() throws {
|
||||
let template = TravelAlbumAIRetouchTemplate(
|
||||
id: 2,
|
||||
name: "简约留白",
|
||||
previewURL: ""
|
||||
func testBeforeAfterComparisonUsesImmersiveAccessibleLayout() throws {
|
||||
let controller = BeforeAfterComparisonViewController(
|
||||
viewModel: BeforeAfterComparisonViewModel(
|
||||
content: BeforeAfterComparisonContent(
|
||||
beforeURL: "https://cdn.example.com/before.jpg",
|
||||
afterURL: "https://cdn.example.com/after.jpg"
|
||||
)
|
||||
)
|
||||
)
|
||||
let controller = TravelAlbumAIRetouchTemplatePreviewViewController(template: template)
|
||||
controller.loadViewIfNeeded()
|
||||
controller.view.frame = CGRect(x: 0, y: 0, width: 390, height: 844)
|
||||
controller.view.layoutIfNeeded()
|
||||
|
||||
let scrollView = try XCTUnwrap(
|
||||
let imageContainer = try XCTUnwrap(
|
||||
controller.view.findSubview {
|
||||
$0.accessibilityIdentifier == "travelAlbum.aiRetouchTemplatePreviewScrollView"
|
||||
} as? UIScrollView
|
||||
$0.accessibilityIdentifier == "travelAlbum.beforeAfterImageContainer"
|
||||
}
|
||||
)
|
||||
let backButton = try XCTUnwrap(
|
||||
controller.view.findSubview {
|
||||
$0.accessibilityIdentifier == "travelAlbum.aiRetouchTemplatePreviewBackButton"
|
||||
$0.accessibilityIdentifier == "travelAlbum.beforeAfterBackButton"
|
||||
} as? UIButton
|
||||
)
|
||||
let divider = try XCTUnwrap(
|
||||
controller.view.findSubview {
|
||||
$0.accessibilityIdentifier == "travelAlbum.beforeAfterDivider"
|
||||
}
|
||||
)
|
||||
|
||||
XCTAssertEqual(controller.view.backgroundColor, UIColor(hex: 0x05070D))
|
||||
XCTAssertTrue(controller.view.allLabels().contains { $0.text == "原图" })
|
||||
XCTAssertTrue(controller.view.allLabels().contains { $0.text == "效果图" })
|
||||
XCTAssertEqual(imageContainer.layer.cornerRadius, 0)
|
||||
XCTAssertTrue(divider.accessibilityTraits.contains(.adjustable))
|
||||
XCTAssertEqual(divider.accessibilityValue, "原图占比 50%")
|
||||
XCTAssertGreaterThanOrEqual(backButton.bounds.width, 44)
|
||||
XCTAssertGreaterThanOrEqual(backButton.bounds.height, 44)
|
||||
}
|
||||
|
||||
func testPreviewComparisonButtonOnlyShowsForSelectedAIResult() throws {
|
||||
UIView.setAnimationsEnabled(false)
|
||||
defer { UIView.setAnimationsEnabled(true) }
|
||||
let project = TravelAlbumPreviewProject(
|
||||
originalMaterialId: 7,
|
||||
assets: [
|
||||
TravelAlbumPreviewAsset(
|
||||
id: "original-7",
|
||||
kind: .original,
|
||||
fileURL: "https://cdn.example.com/original.jpg",
|
||||
coverURL: "",
|
||||
fileName: "原图.jpg",
|
||||
fileSize: 10
|
||||
),
|
||||
TravelAlbumPreviewAsset(
|
||||
id: "retouched-7",
|
||||
kind: .retouched,
|
||||
fileURL: "https://cdn.example.com/retouched.jpg",
|
||||
coverURL: "",
|
||||
fileName: "精修.jpg",
|
||||
fileSize: 10
|
||||
),
|
||||
]
|
||||
)
|
||||
let controller = TravelAlbumPhotoPreviewViewController(
|
||||
projects: [project],
|
||||
totalCount: 1,
|
||||
startProjectIndex: 0
|
||||
)
|
||||
controller.loadViewIfNeeded()
|
||||
controller.view.frame = CGRect(x: 0, y: 0, width: 390, height: 844)
|
||||
controller.view.layoutIfNeeded()
|
||||
let segmentedControl = try XCTUnwrap(
|
||||
controller.view.findSubview {
|
||||
$0.accessibilityIdentifier == "travelAlbum.previewVariantSegmentedControl"
|
||||
} as? UISegmentedControl
|
||||
)
|
||||
let comparisonButton = try XCTUnwrap(
|
||||
controller.view.findSubview {
|
||||
$0.accessibilityIdentifier == "travelAlbum.previewComparisonButton"
|
||||
} as? UIButton
|
||||
)
|
||||
|
||||
XCTAssertEqual(controller.view.backgroundColor, .black)
|
||||
XCTAssertTrue(controller.view.allLabels().contains { $0.text == "简约留白" })
|
||||
XCTAssertTrue(controller.view.allLabels().contains { $0.text == "双指缩放查看细节" })
|
||||
XCTAssertEqual(scrollView.minimumZoomScale, 1)
|
||||
XCTAssertEqual(scrollView.maximumZoomScale, 4)
|
||||
XCTAssertGreaterThanOrEqual(backButton.bounds.width, 44)
|
||||
XCTAssertGreaterThanOrEqual(backButton.bounds.height, 44)
|
||||
XCTAssertTrue(comparisonButton.isHidden)
|
||||
segmentedControl.selectedSegmentIndex = 1
|
||||
segmentedControl.sendActions(for: .valueChanged)
|
||||
|
||||
XCTAssertFalse(comparisonButton.isHidden)
|
||||
XCTAssertNil(comparisonButton.configuration?.title)
|
||||
XCTAssertNotNil(comparisonButton.configuration?.image)
|
||||
XCTAssertEqual(comparisonButton.bounds.size, CGSize(width: 48, height: 48))
|
||||
}
|
||||
|
||||
func testPreviewAIRetouchUsesSelectedRetouchedTabWorkflowAndStaysPresented() async throws {
|
||||
|
||||
@@ -8,6 +8,40 @@ import XCTest
|
||||
|
||||
/// 旅拍相册模型与展示工具测试。
|
||||
final class TravelAlbumModelsTests: XCTestCase {
|
||||
func testBeforeAfterComparisonMotionMatchesShipSwiftInteractionRange() {
|
||||
XCTAssertEqual(BeforeAfterComparisonMotion.automaticFraction(elapsed: 0), 0.5, accuracy: 0.001)
|
||||
XCTAssertEqual(
|
||||
BeforeAfterComparisonMotion.automaticFraction(
|
||||
elapsed: .pi / (2 * BeforeAfterComparisonMotion.automaticSpeed)
|
||||
),
|
||||
0.8,
|
||||
accuracy: 0.001
|
||||
)
|
||||
XCTAssertEqual(BeforeAfterComparisonMotion.clampedManualFraction(0), 0.05)
|
||||
XCTAssertEqual(BeforeAfterComparisonMotion.clampedManualFraction(1), 0.95)
|
||||
XCTAssertEqual(BeforeAfterComparisonMotion.clampedManualFraction(0.42), 0.42)
|
||||
}
|
||||
|
||||
func testBeforeAfterComparisonLayoutFitsVariableRatiosWithoutCropping() {
|
||||
let landscape = BeforeAfterComparisonLayout.fittedSize(
|
||||
aspectRatio: 16.0 / 9.0,
|
||||
maximumSize: CGSize(width: 358, height: 580)
|
||||
)
|
||||
let portrait = BeforeAfterComparisonLayout.fittedSize(
|
||||
aspectRatio: 2.0 / 3.0,
|
||||
maximumSize: CGSize(width: 358, height: 580)
|
||||
)
|
||||
|
||||
XCTAssertEqual(landscape.width, 358, accuracy: 0.001)
|
||||
XCTAssertEqual(landscape.height, 201.375, accuracy: 0.001)
|
||||
XCTAssertEqual(portrait.width, 358, accuracy: 0.001)
|
||||
XCTAssertEqual(portrait.height, 537, accuracy: 0.001)
|
||||
XCTAssertEqual(
|
||||
BeforeAfterComparisonLayout.aspectRatio(for: CGSize(width: 4000, height: 3000)),
|
||||
4.0 / 3.0
|
||||
)
|
||||
}
|
||||
|
||||
func testPreviewProjectMapsMaterialToOriginalAsset() {
|
||||
let material = TravelAlbumMaterial(
|
||||
id: 8,
|
||||
@@ -68,6 +102,77 @@ final class TravelAlbumModelsTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testPreviewProjectBuildsComparisonOnlyForValidAIResults() {
|
||||
let project = makePreviewProject(id: 8, kinds: [.original, .retouched, .atmosphere, .cover])
|
||||
|
||||
XCTAssertEqual(
|
||||
project.comparisonContent(for: .retouched),
|
||||
BeforeAfterComparisonContent(
|
||||
beforeURL: "https://cdn.example.com/8-0.jpg",
|
||||
afterURL: "https://cdn.example.com/8-1.jpg"
|
||||
)
|
||||
)
|
||||
XCTAssertNotNil(project.comparisonContent(for: .atmosphere))
|
||||
XCTAssertNil(project.comparisonContent(for: .original))
|
||||
XCTAssertNil(project.comparisonContent(for: .cover))
|
||||
|
||||
let missingResultURL = TravelAlbumPreviewProject(
|
||||
originalMaterialId: 9,
|
||||
assets: [
|
||||
TravelAlbumPreviewAsset(
|
||||
id: "original-9",
|
||||
kind: .original,
|
||||
fileURL: "https://cdn.example.com/original.jpg",
|
||||
coverURL: "",
|
||||
fileName: "原图.jpg",
|
||||
fileSize: 1
|
||||
),
|
||||
TravelAlbumPreviewAsset(
|
||||
id: "retouched-9",
|
||||
kind: .retouched,
|
||||
fileURL: " ",
|
||||
coverURL: "",
|
||||
fileName: "精修.jpg",
|
||||
fileSize: 1
|
||||
),
|
||||
]
|
||||
)
|
||||
XCTAssertNil(missingResultURL.comparisonContent(for: .retouched))
|
||||
}
|
||||
|
||||
func testTemplateComparisonContentRequiresBothURLs() {
|
||||
let valid = TravelAlbumAIRetouchTemplate(
|
||||
id: 1,
|
||||
name: "清透",
|
||||
previewURL: "https://cdn.example.com/preview.jpg",
|
||||
beforeURL: " https://cdn.example.com/before.jpg ",
|
||||
afterURL: "https://cdn.example.com/after.jpg"
|
||||
)
|
||||
let missingAfter = TravelAlbumAIRetouchTemplate(
|
||||
id: 2,
|
||||
name: "暖阳",
|
||||
previewURL: "https://cdn.example.com/preview.jpg",
|
||||
beforeURL: "https://cdn.example.com/before.jpg"
|
||||
)
|
||||
|
||||
XCTAssertEqual(valid.comparisonContent?.beforeURL, "https://cdn.example.com/before.jpg")
|
||||
XCTAssertEqual(valid.comparisonContent?.afterLabel, "效果图")
|
||||
XCTAssertNil(missingAfter.comparisonContent)
|
||||
}
|
||||
|
||||
func testTemplateDecodingDefaultsMissingComparisonURLsToEmptyStrings() throws {
|
||||
let data = Data(
|
||||
#"{"id":3,"name":"旧模板","preview_url":"https://cdn.example.com/preview.jpg"}"#.utf8
|
||||
)
|
||||
|
||||
let template = try JSONDecoder().decode(TravelAlbumAIRetouchTemplate.self, from: data)
|
||||
|
||||
XCTAssertEqual(template.previewURL, "https://cdn.example.com/preview.jpg")
|
||||
XCTAssertEqual(template.beforeURL, "")
|
||||
XCTAssertEqual(template.afterURL, "")
|
||||
XCTAssertNil(template.comparisonContent)
|
||||
}
|
||||
|
||||
func testPreviewAssetFallsBackToAvailableURLForBothQualityLevels() {
|
||||
let missingOriginal = TravelAlbumPreviewAsset(
|
||||
id: "cover-only",
|
||||
|
||||
Reference in New Issue
Block a user