新增ai修图
This commit is contained in:
@ -4,6 +4,7 @@
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import UIKit
|
||||
@testable import suixinkan
|
||||
|
||||
@MainActor
|
||||
@ -75,6 +76,208 @@ final class TravelAlbumEntryViewModelTests: XCTestCase {
|
||||
@MainActor
|
||||
/// 旅拍相册详情 ViewModel 测试。
|
||||
final class TravelAlbumDetailViewModelTests: XCTestCase {
|
||||
func testAIEditResultDisablesDuringProcessingAndOverwritesPreviousResult() {
|
||||
var state = TravelAlbumAIEditResultState()
|
||||
|
||||
XCTAssertTrue(state.canStartEditing)
|
||||
XCTAssertFalse(state.hasEditedResult)
|
||||
XCTAssertEqual(state.thumbnailStatus, .none)
|
||||
|
||||
state.startProcessing()
|
||||
XCTAssertFalse(state.canStartEditing)
|
||||
XCTAssertEqual(state.thumbnailStatus, .processing)
|
||||
state.complete(presetID: "portrait")
|
||||
XCTAssertTrue(state.canStartEditing)
|
||||
XCTAssertEqual(state.appliedPresetID, "portrait")
|
||||
XCTAssertEqual(state.displayMode, .edited)
|
||||
XCTAssertEqual(state.thumbnailStatus, .edited)
|
||||
|
||||
state.selectDisplayMode(.original)
|
||||
XCTAssertEqual(state.displayMode, .original)
|
||||
state.startProcessing()
|
||||
XCTAssertEqual(state.appliedPresetID, "portrait")
|
||||
state.complete(presetID: "mist")
|
||||
XCTAssertEqual(state.appliedPresetID, "mist")
|
||||
XCTAssertEqual(state.displayMode, .edited)
|
||||
}
|
||||
|
||||
func testAIEditFailureShowsFailureStatusAndKeepsPreviousResult() {
|
||||
var state = TravelAlbumAIEditResultState()
|
||||
state.complete(presetID: "portrait")
|
||||
|
||||
state.startProcessing()
|
||||
state.failProcessing()
|
||||
|
||||
XCTAssertTrue(state.canStartEditing)
|
||||
XCTAssertTrue(state.hasEditedResult)
|
||||
XCTAssertEqual(state.appliedPresetID, "portrait")
|
||||
XCTAssertEqual(state.thumbnailStatus, .failed)
|
||||
|
||||
state.startProcessing()
|
||||
XCTAssertEqual(state.thumbnailStatus, .processing)
|
||||
}
|
||||
|
||||
func testRestoreOriginalClearsEditedThumbnailStatus() {
|
||||
var state = TravelAlbumAIEditResultState()
|
||||
state.complete(presetID: "portrait")
|
||||
|
||||
state.startProcessing()
|
||||
state.restoreOriginal()
|
||||
|
||||
XCTAssertTrue(state.canStartEditing)
|
||||
XCTAssertFalse(state.hasEditedResult)
|
||||
XCTAssertEqual(state.displayMode, .original)
|
||||
XCTAssertEqual(state.thumbnailStatus, .none)
|
||||
}
|
||||
|
||||
func testCoverResultUsesDistinctThumbnailStatusAndCanBeOverwritten() {
|
||||
var state = TravelAlbumAIEditResultState()
|
||||
|
||||
state.complete(presetID: "portrait", coverTemplateID: "travel_memoir")
|
||||
XCTAssertTrue(state.isCover)
|
||||
XCTAssertEqual(state.appliedCoverTemplateID, "travel_memoir")
|
||||
XCTAssertEqual(state.displayMode, .cover)
|
||||
XCTAssertEqual(state.thumbnailStatus, .cover)
|
||||
|
||||
state.complete(presetID: "mist")
|
||||
XCTAssertFalse(state.isCover)
|
||||
XCTAssertNil(state.appliedCoverTemplateID)
|
||||
XCTAssertEqual(state.thumbnailStatus, .edited)
|
||||
}
|
||||
|
||||
func testAtmosphereResultCanBeSelectedAndIsClearedByNextSingleResult() {
|
||||
var state = TravelAlbumAIEditResultState()
|
||||
|
||||
state.complete(presetID: "portrait", atmosphereOptionID: "atmosphere")
|
||||
XCTAssertTrue(state.hasAtmosphereResult)
|
||||
state.selectDisplayMode(.atmosphere)
|
||||
XCTAssertEqual(state.displayMode, .atmosphere)
|
||||
|
||||
state.complete(presetID: "mist")
|
||||
XCTAssertFalse(state.hasAtmosphereResult)
|
||||
XCTAssertNil(state.appliedAtmosphereOptionID)
|
||||
state.selectDisplayMode(.atmosphere)
|
||||
XCTAssertEqual(state.displayMode, .edited)
|
||||
}
|
||||
|
||||
func testAIEditResultStoreSharesAndOverwritesLatestImage() {
|
||||
let materialID = 990_071
|
||||
let store = TravelAlbumAIEditResultStore.shared
|
||||
store.remove(materialID: materialID)
|
||||
|
||||
store.startProcessing(materialIDs: [materialID])
|
||||
XCTAssertFalse(store.state(for: materialID).canStartEditing)
|
||||
|
||||
let firstImage = UIImage()
|
||||
store.complete(materialID: materialID, presetID: "portrait", editedImage: firstImage)
|
||||
XCTAssertTrue(store.editedImage(for: materialID) === firstImage)
|
||||
|
||||
let replacementImage = UIImage()
|
||||
let atmosphereImage = UIImage()
|
||||
let coverImage = UIImage()
|
||||
store.startProcessing(materialIDs: [materialID])
|
||||
store.complete(
|
||||
materialID: materialID,
|
||||
presetID: "mist",
|
||||
atmosphereOptionID: "atmosphere",
|
||||
coverTemplateID: "travel_memoir",
|
||||
editedImage: replacementImage,
|
||||
atmosphereImage: atmosphereImage,
|
||||
coverImage: coverImage
|
||||
)
|
||||
XCTAssertEqual(store.state(for: materialID).appliedPresetID, "mist")
|
||||
XCTAssertTrue(store.editedImage(for: materialID) === replacementImage)
|
||||
XCTAssertTrue(store.atmosphereImage(for: materialID) === atmosphereImage)
|
||||
XCTAssertTrue(store.coverImage(for: materialID) === coverImage)
|
||||
store.remove(materialID: materialID)
|
||||
}
|
||||
|
||||
func testSelectionToolbarReplacesUploadWithAIEditAndDelete() {
|
||||
let normalState = TravelAlbumSelectionToolbarState(isSelectionMode: false, selectedCount: 0)
|
||||
XCTAssertTrue(normalState.showsUpload)
|
||||
XCTAssertFalse(normalState.showsSelectionActions)
|
||||
|
||||
let emptySelectionState = TravelAlbumSelectionToolbarState(isSelectionMode: true, selectedCount: 0)
|
||||
XCTAssertFalse(emptySelectionState.showsUpload)
|
||||
XCTAssertTrue(emptySelectionState.showsSelectionActions)
|
||||
XCTAssertFalse(emptySelectionState.selectionActionsEnabled)
|
||||
|
||||
let selectedState = TravelAlbumSelectionToolbarState(isSelectionMode: true, selectedCount: 2)
|
||||
XCTAssertTrue(selectedState.showsSelectionActions)
|
||||
XCTAssertTrue(selectedState.selectionActionsEnabled)
|
||||
}
|
||||
|
||||
func testPhotoPreviewCanSwipeAndKeepsValidIndexAfterDelete() {
|
||||
let materials = [
|
||||
TravelAlbumMaterial(id: 1, fileName: "A.JPG"),
|
||||
TravelAlbumMaterial(id: 2, fileName: "B.JPG"),
|
||||
TravelAlbumMaterial(id: 3, fileName: "C.JPG"),
|
||||
]
|
||||
var state = TravelAlbumPhotoPreviewState(materials: materials, initialIndex: 1)
|
||||
|
||||
XCTAssertEqual(state.currentMaterial?.id, 2)
|
||||
XCTAssertTrue(state.moveNext())
|
||||
XCTAssertEqual(state.currentMaterial?.id, 3)
|
||||
XCTAssertFalse(state.moveNext())
|
||||
XCTAssertTrue(state.removeCurrent())
|
||||
XCTAssertEqual(state.currentMaterial?.id, 2)
|
||||
XCTAssertTrue(state.movePrevious())
|
||||
XCTAssertEqual(state.currentMaterial?.id, 1)
|
||||
}
|
||||
|
||||
func testAIEditPresetsContainScrollableChoices() {
|
||||
let presets = TravelAlbumEditPreset.defaultOptions
|
||||
|
||||
XCTAssertEqual(presets.count, 8)
|
||||
XCTAssertEqual(Set(presets.map(\.id)).count, presets.count)
|
||||
XCTAssertEqual(presets.first?.effect, .original)
|
||||
XCTAssertTrue(presets.contains { $0.effect == .mist })
|
||||
}
|
||||
|
||||
func testAtmosphereOptionsContainScrollableSingleChoiceStyles() {
|
||||
let options = TravelAlbumAtmosphereEditOption.defaultOptions
|
||||
|
||||
XCTAssertGreaterThanOrEqual(options.count, 4)
|
||||
XCTAssertEqual(Set(options.map(\.id)).count, options.count)
|
||||
XCTAssertTrue(options.allSatisfy { $0.effect != .original })
|
||||
XCTAssertEqual(TravelAlbumAtmosphereEditOption.defaultOption, options.first)
|
||||
}
|
||||
|
||||
func testAIEditSelectionCountsOptionalAtmosphereAndCoverStyles() {
|
||||
let refinedPreset = TravelAlbumEditPreset.defaultOptions[1]
|
||||
let baseSelection = TravelAlbumAIEditSelection(
|
||||
refinedPreset: refinedPreset,
|
||||
atmosphereOption: nil,
|
||||
coverTemplate: nil
|
||||
)
|
||||
XCTAssertEqual(baseSelection.resultCountPerPhoto, 1)
|
||||
XCTAssertEqual(baseSelection.selectedStyleCount, 1)
|
||||
|
||||
let fullSelection = TravelAlbumAIEditSelection(
|
||||
refinedPreset: refinedPreset,
|
||||
atmosphereOption: .defaultOption,
|
||||
coverTemplate: TravelAlbumCoverTemplate.defaultOptions[0]
|
||||
)
|
||||
XCTAssertEqual(fullSelection.resultCountPerPhoto, 2)
|
||||
XCTAssertEqual(fullSelection.selectedStyleCount, 3)
|
||||
}
|
||||
|
||||
func testCoverTemplatesAppearFromFourSelectedPhotos() {
|
||||
XCTAssertFalse(TravelAlbumCoverTemplate.shouldShow(for: 3))
|
||||
XCTAssertTrue(TravelAlbumCoverTemplate.shouldShow(for: 4))
|
||||
XCTAssertTrue(TravelAlbumCoverTemplate.shouldShow(for: 8))
|
||||
}
|
||||
|
||||
func testCoverTemplatesAreFreeAndReadyForFutureQuotaPricing() {
|
||||
let templates = TravelAlbumCoverTemplate.defaultOptions
|
||||
|
||||
XCTAssertEqual(templates.count, 4)
|
||||
XCTAssertEqual(Set(templates.map(\.id)).count, templates.count)
|
||||
XCTAssertEqual(templates.first?.id, "travel_memoir")
|
||||
XCTAssertTrue(templates.allSatisfy(\.isFreeGift))
|
||||
XCTAssertTrue(templates.allSatisfy { $0.quotaCost == 0 })
|
||||
}
|
||||
|
||||
func testRefreshLoadsInfoAndMaterials() async {
|
||||
let api = TravelAlbumMockAPI()
|
||||
api.infoResponse = TravelAlbum(id: 2, name: "详情")
|
||||
@ -106,6 +309,53 @@ final class TravelAlbumDetailViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(api.materialRequests.last?.orderBy, 4)
|
||||
}
|
||||
|
||||
func testPurchasedTabUsesLocalDemoMaterialsWhenServerIsEmpty() async {
|
||||
let api = TravelAlbumMockAPI()
|
||||
api.materialListResponses = [TravelAlbumListResponse(total: 0, list: [])]
|
||||
let viewModel = TravelAlbumDetailViewModel(albumId: 2)
|
||||
|
||||
await viewModel.selectTab(.purchased, api: api)
|
||||
|
||||
XCTAssertTrue(viewModel.isUsingPurchasedDemoData)
|
||||
XCTAssertEqual(viewModel.materials.count, 4)
|
||||
XCTAssertTrue(viewModel.materials.allSatisfy(\.isPurchased))
|
||||
XCTAssertTrue(viewModel.materials.allSatisfy { $0.id < 0 })
|
||||
XCTAssertTrue(viewModel.materials.allSatisfy { $0.bundledImageAssetName != nil })
|
||||
}
|
||||
|
||||
func testPurchasedTabKeepsRealServerMaterialsWhenAvailable() async {
|
||||
let api = TravelAlbumMockAPI()
|
||||
let serverMaterial = TravelAlbumMaterial(
|
||||
id: 88,
|
||||
fileName: "SERVER_0088.JPG",
|
||||
fileUrl: "https://cdn.example.com/server-88.jpg",
|
||||
isPurchased: true
|
||||
)
|
||||
api.materialListResponses = [
|
||||
TravelAlbumListResponse(total: 1, list: [serverMaterial]),
|
||||
]
|
||||
let viewModel = TravelAlbumDetailViewModel(albumId: 2)
|
||||
|
||||
await viewModel.selectTab(.purchased, api: api)
|
||||
|
||||
XCTAssertFalse(viewModel.isUsingPurchasedDemoData)
|
||||
XCTAssertEqual(viewModel.materials, [serverMaterial])
|
||||
}
|
||||
|
||||
func testPurchasedDemoMaterialsFollowCurrentSortOption() async {
|
||||
let api = TravelAlbumMockAPI()
|
||||
api.materialListResponses = [
|
||||
TravelAlbumListResponse(total: 0, list: []),
|
||||
TravelAlbumListResponse(total: 0, list: []),
|
||||
]
|
||||
let viewModel = TravelAlbumDetailViewModel(albumId: 2)
|
||||
|
||||
await viewModel.selectTab(.purchased, api: api)
|
||||
await viewModel.setSortOption(.fileNameAsc, api: api)
|
||||
|
||||
XCTAssertEqual(viewModel.materials.map(\.fileName), viewModel.materials.map(\.fileName).sorted())
|
||||
}
|
||||
|
||||
func testOnlyUnpurchasedMaterialCanBeSelected() {
|
||||
let viewModel = TravelAlbumDetailViewModel(albumId: 2)
|
||||
viewModel.toggleSelectionMode()
|
||||
@ -170,6 +420,29 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(secondViewModel.transferModeOption, "拍后传输")
|
||||
}
|
||||
|
||||
func testInitialTransferModeOverridesPersistedModeAndBecomesLatestSelection() {
|
||||
let defaults = makeOTGTestDefaults()
|
||||
defaults.set(TravelAlbumOTGTransferMode.liveUpload.rawValue, forKey: "travelAlbum.otg.transferMode")
|
||||
let firstViewModel = WiredCameraTransferViewModel(
|
||||
albumId: 9,
|
||||
albumTitle: "相册",
|
||||
headerPhone: "13800138000",
|
||||
initialTransferMode: .postTransfer,
|
||||
userDefaults: defaults
|
||||
)
|
||||
|
||||
XCTAssertEqual(firstViewModel.transferMode, .postTransfer)
|
||||
XCTAssertEqual(firstViewModel.transferModeOption, "拍后传输")
|
||||
|
||||
let secondViewModel = WiredCameraTransferViewModel(
|
||||
albumId: 18,
|
||||
albumTitle: "另一个相册",
|
||||
headerPhone: "13800138000",
|
||||
userDefaults: defaults
|
||||
)
|
||||
XCTAssertEqual(secondViewModel.transferMode, .postTransfer)
|
||||
}
|
||||
|
||||
func testConnectedStatusIncludesCameraModelName() {
|
||||
let context = makeOTGTestContext()
|
||||
let viewModel = makeWiredViewModel(
|
||||
@ -210,6 +483,31 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(groups.first?.slots.last?.photoCount, 2)
|
||||
}
|
||||
|
||||
func testPhotoPreviewContextPreservesListOrderAndStartsAtTappedPhoto() {
|
||||
let photos = [
|
||||
makeOTGPhotoItem(
|
||||
id: "a",
|
||||
capturedAt: "2026-07-08 12:30:00",
|
||||
thumbnailURL: URL(fileURLWithPath: "/tmp/a.jpg")
|
||||
),
|
||||
makeOTGPhotoItem(
|
||||
id: "unavailable",
|
||||
capturedAt: "2026-07-08 12:20:00"
|
||||
),
|
||||
makeOTGPhotoItem(
|
||||
id: "c",
|
||||
capturedAt: "2026-07-08 12:10:00",
|
||||
thumbnailURL: URL(string: "https://cdn.example.com/c.jpg")
|
||||
),
|
||||
]
|
||||
|
||||
let context = photos.previewContext(startingWith: "c")
|
||||
|
||||
XCTAssertEqual(context?.photos.map(\.id), ["a", "c"])
|
||||
XCTAssertEqual(context?.initialIndex, 1)
|
||||
XCTAssertNil(photos.previewContext(startingWith: "unavailable"))
|
||||
}
|
||||
|
||||
func testSelectedTimeSlotFallsBackWhenTabChanges() throws {
|
||||
let context = makeOTGTestContext()
|
||||
context.store.save([
|
||||
@ -741,13 +1039,14 @@ private extension TravelAlbumOTGPhotoRecord {
|
||||
private func makeOTGPhotoItem(
|
||||
id: String,
|
||||
capturedAt: String,
|
||||
status: TravelAlbumOTGUploadStatus = .pending
|
||||
status: TravelAlbumOTGUploadStatus = .pending,
|
||||
thumbnailURL: URL? = nil
|
||||
) -> TravelAlbumOTGPhotoItem {
|
||||
TravelAlbumOTGPhotoItem(
|
||||
id: id,
|
||||
sourceId: id,
|
||||
fileName: "\(id).JPG",
|
||||
thumbnailURL: nil,
|
||||
thumbnailURL: thumbnailURL,
|
||||
capturedAt: capturedAt,
|
||||
fileSizeText: "1 KB",
|
||||
fileSizeBytes: 1024,
|
||||
|
||||
Reference in New Issue
Block a user