新增线下收款记录和 ai 修图优化
This commit is contained in:
@@ -59,6 +59,55 @@ final class TravelAlbumEntryViewModelTests: XCTestCase {
|
||||
XCTAssertFalse(viewModel.isCreateSheetVisible)
|
||||
}
|
||||
|
||||
func testCreateSuccessPersistsSelectedAutoRetouchTemplate() async {
|
||||
let api = TravelAlbumMockAPI()
|
||||
api.createResponse = TravelAlbumCreateResponse(id: 19)
|
||||
let defaults = makeOTGTestDefaults()
|
||||
let configurationStore = TravelAlbumAutoRetouchConfigurationStore(userDefaults: defaults)
|
||||
let viewModel = TravelAlbumEntryViewModel(
|
||||
currentScenicIdProvider: { 1 },
|
||||
autoRetouchConfigurationStore: configurationStore
|
||||
)
|
||||
|
||||
await viewModel.confirmCreate(
|
||||
mode: .preShoot,
|
||||
freeCount: "2",
|
||||
singlePrice: "12.5",
|
||||
packagePrice: "88",
|
||||
autoRetouchConfiguration: .enabled(templateID: "brocade"),
|
||||
order: nil,
|
||||
api: api
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
configurationStore.configuration(albumID: 19),
|
||||
.enabled(templateID: "brocade")
|
||||
)
|
||||
}
|
||||
|
||||
func testCreateRejectsEnabledAutoRetouchWithoutValidTemplate() async {
|
||||
let api = TravelAlbumMockAPI()
|
||||
let viewModel = TravelAlbumEntryViewModel(currentScenicIdProvider: { 1 })
|
||||
var message = ""
|
||||
viewModel.onShowMessage = { message = $0 }
|
||||
|
||||
await viewModel.confirmCreate(
|
||||
mode: .preShoot,
|
||||
freeCount: "",
|
||||
singlePrice: "12.5",
|
||||
packagePrice: "",
|
||||
autoRetouchConfiguration: TravelAlbumAutoRetouchConfiguration(
|
||||
isEnabled: true,
|
||||
templateID: "missing-template"
|
||||
),
|
||||
order: nil,
|
||||
api: api
|
||||
)
|
||||
|
||||
XCTAssertEqual(message, "请选择修图模板")
|
||||
XCTAssertTrue(api.createRequests.isEmpty)
|
||||
}
|
||||
|
||||
func testOrderAlreadyBoundReloadsOrders() async {
|
||||
let api = TravelAlbumMockAPI()
|
||||
api.createError = APIError.serverCode(100001, "订单已关联")
|
||||
@@ -73,6 +122,31 @@ final class TravelAlbumEntryViewModelTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// 自动修图选择弹窗的界面状态测试。
|
||||
final class TravelAlbumAutoRetouchSettingSheetTests: XCTestCase {
|
||||
func testSelectingTemplateImmediatelyRefreshesSelectedAppearance() throws {
|
||||
let controller = TravelAlbumAutoRetouchSettingSheetViewController(
|
||||
configuration: .disabled,
|
||||
startsWithModeSelection: false
|
||||
)
|
||||
controller.loadViewIfNeeded()
|
||||
controller.view.frame = CGRect(x: 0, y: 0, width: 402, height: 874)
|
||||
controller.view.layoutIfNeeded()
|
||||
|
||||
let tableView = try XCTUnwrap(
|
||||
controller.view.subviews.compactMap { $0 as? UITableView }.first
|
||||
)
|
||||
let selectedIndexPath = IndexPath(row: 1, section: 0)
|
||||
|
||||
tableView.delegate?.tableView?(tableView, didSelectRowAt: selectedIndexPath)
|
||||
controller.view.layoutIfNeeded()
|
||||
|
||||
XCTAssertEqual(tableView.cellForRow(at: selectedIndexPath)?.accessibilityValue, "已选择")
|
||||
XCTAssertEqual(tableView.cellForRow(at: IndexPath(row: 0, section: 0))?.accessibilityValue, "未选择")
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// 旅拍相册详情 ViewModel 测试。
|
||||
final class TravelAlbumDetailViewModelTests: XCTestCase {
|
||||
@@ -785,6 +859,134 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
|
||||
XCTAssertTrue(records.allSatisfy { UUID(uuidString: $0.clientPhotoId) != nil })
|
||||
}
|
||||
|
||||
func testAutoRetouchConfigurationLoadsAndUpdatesUploadChip() {
|
||||
let context = makeOTGTestContext()
|
||||
let defaults = makeOTGTestDefaults()
|
||||
let configurationStore = TravelAlbumAutoRetouchConfigurationStore(userDefaults: defaults)
|
||||
configurationStore.save(.enabled(templateID: "brocade"), albumID: 9)
|
||||
let viewModel = makeWiredViewModel(
|
||||
context: context,
|
||||
manager: MockWiredCameraConnectionManager(driver: MockCameraDriver(objects: [])),
|
||||
userDefaults: defaults,
|
||||
autoRetouchConfigurationStore: configurationStore
|
||||
)
|
||||
|
||||
XCTAssertEqual(viewModel.retouchOption, "AI 修图")
|
||||
|
||||
viewModel.updateAutoRetouchConfiguration(.disabled)
|
||||
|
||||
XCTAssertEqual(viewModel.retouchOption, "不修图")
|
||||
XCTAssertEqual(configurationStore.configuration(albumID: 9), .disabled)
|
||||
}
|
||||
|
||||
func testAutoRetouchFirstLevelOnlyContainsNoRetouchAndAIRetouch() {
|
||||
XCTAssertEqual(TravelAlbumRetouchMode.allCases, [.disabled, .aiRetouch])
|
||||
XCTAssertEqual(TravelAlbumRetouchMode.disabled.title, "不修图")
|
||||
XCTAssertEqual(TravelAlbumRetouchMode.aiRetouch.title, "AI 修图")
|
||||
}
|
||||
|
||||
func testUploadAutomaticallyRetouchesAndPersistsEditedResult() async throws {
|
||||
let context = makeOTGTestContext()
|
||||
defer { try? FileManager.default.removeItem(at: context.root) }
|
||||
let defaults = makeOTGTestDefaults()
|
||||
let configurationStore = TravelAlbumAutoRetouchConfigurationStore(userDefaults: defaults)
|
||||
configurationStore.save(.enabled(templateID: "brocade"), albumID: 9)
|
||||
let materialID = 990_901
|
||||
let uploader = MockTravelAlbumOTGUploader(materialID: materialID)
|
||||
let processor = MockTravelAlbumAutoRetouchProcessor()
|
||||
let resultStore = TravelAlbumAIEditResultStore.shared
|
||||
resultStore.remove(materialID: materialID)
|
||||
defer { resultStore.remove(materialID: materialID) }
|
||||
let record = try makePersistedOTGRecord(
|
||||
context: context,
|
||||
id: "auto-retouch",
|
||||
capturedAt: "2026-07-08 12:05:00",
|
||||
status: .pending
|
||||
)
|
||||
context.store.save([record], albumId: 9)
|
||||
let viewModel = makeWiredViewModel(
|
||||
context: context,
|
||||
manager: MockWiredCameraConnectionManager(driver: MockCameraDriver(objects: [])),
|
||||
uploader: uploader,
|
||||
userDefaults: defaults,
|
||||
autoRetouchConfigurationStore: configurationStore,
|
||||
autoRetouchProcessor: processor
|
||||
)
|
||||
|
||||
viewModel.reloadLocalPhotos()
|
||||
viewModel.onSpecifyUploadOptionSelected(.allPending)
|
||||
await waitUntil(timeout: 2) {
|
||||
context.store.load(albumId: 9).first?.autoRetouchState == .completed
|
||||
}
|
||||
|
||||
let saved = try XCTUnwrap(context.store.load(albumId: 9).first)
|
||||
XCTAssertEqual(saved.status, .uploaded)
|
||||
XCTAssertEqual(saved.materialId, materialID)
|
||||
XCTAssertEqual(saved.autoRetouchState, .completed)
|
||||
XCTAssertEqual(saved.autoRetouchTemplateId, "brocade")
|
||||
XCTAssertTrue(context.store.fileExists(relativePath: saved.retouchedPath, albumId: 9))
|
||||
XCTAssertEqual(processor.processedPresetIDs, ["brocade"])
|
||||
XCTAssertEqual(resultStore.state(for: materialID).appliedPresetID, "brocade")
|
||||
XCTAssertEqual(viewModel.photos.first?.autoRetouchState, .completed)
|
||||
|
||||
viewModel.deletePhoto(photoId: saved.id)
|
||||
|
||||
XCTAssertFalse(context.store.fileExists(relativePath: saved.retouchedPath, albumId: 9))
|
||||
XCTAssertFalse(resultStore.state(for: materialID).hasEditedResult)
|
||||
}
|
||||
|
||||
func testAutoRetouchFailureKeepsUploadSuccessfulAndCanRetryWithoutReupload() async throws {
|
||||
let context = makeOTGTestContext()
|
||||
defer { try? FileManager.default.removeItem(at: context.root) }
|
||||
let defaults = makeOTGTestDefaults()
|
||||
let configurationStore = TravelAlbumAutoRetouchConfigurationStore(userDefaults: defaults)
|
||||
configurationStore.save(.enabled(templateID: "mist"), albumID: 9)
|
||||
let materialID = 990_902
|
||||
let resultStore = TravelAlbumAIEditResultStore.shared
|
||||
resultStore.remove(materialID: materialID)
|
||||
defer { resultStore.remove(materialID: materialID) }
|
||||
let uploader = MockTravelAlbumOTGUploader(materialID: materialID)
|
||||
let processor = MockTravelAlbumAutoRetouchProcessor()
|
||||
processor.shouldFail = true
|
||||
let record = try makePersistedOTGRecord(
|
||||
context: context,
|
||||
id: "retry-retouch",
|
||||
capturedAt: "2026-07-08 12:05:00",
|
||||
status: .pending
|
||||
)
|
||||
context.store.save([record], albumId: 9)
|
||||
let viewModel = makeWiredViewModel(
|
||||
context: context,
|
||||
manager: MockWiredCameraConnectionManager(driver: MockCameraDriver(objects: [])),
|
||||
uploader: uploader,
|
||||
userDefaults: defaults,
|
||||
autoRetouchConfigurationStore: configurationStore,
|
||||
autoRetouchProcessor: processor
|
||||
)
|
||||
|
||||
viewModel.reloadLocalPhotos()
|
||||
viewModel.onSpecifyUploadOptionSelected(.allPending)
|
||||
await waitUntil(timeout: 2) {
|
||||
context.store.load(albumId: 9).first?.autoRetouchState == .failed
|
||||
}
|
||||
|
||||
var saved = try XCTUnwrap(context.store.load(albumId: 9).first)
|
||||
XCTAssertEqual(saved.status, .uploaded)
|
||||
XCTAssertEqual(viewModel.tabCounts[TravelAlbumOTGTransferTab.failed.rawValue], 0)
|
||||
XCTAssertEqual(uploader.uploadCallCount, 1)
|
||||
|
||||
processor.shouldFail = false
|
||||
viewModel.retryAutoRetouch(photoId: saved.id)
|
||||
await waitUntil(timeout: 2) {
|
||||
context.store.load(albumId: 9).first?.autoRetouchState == .completed
|
||||
}
|
||||
|
||||
saved = try XCTUnwrap(context.store.load(albumId: 9).first)
|
||||
XCTAssertEqual(saved.autoRetouchState, .completed)
|
||||
XCTAssertEqual(uploader.uploadCallCount, 1)
|
||||
XCTAssertEqual(processor.processedPresetIDs, ["mist", "mist"])
|
||||
}
|
||||
|
||||
func testStartSynchronizesPersistedUploadStatusesFromServerClientPhotoIds() async {
|
||||
let context = makeOTGTestContext()
|
||||
let api = TravelAlbumMockAPI()
|
||||
@@ -1013,9 +1215,13 @@ private func makeWiredViewModel(
|
||||
manager: MockWiredCameraConnectionManager,
|
||||
uploader: MockTravelAlbumOTGUploader? = nil,
|
||||
api: TravelAlbumMockAPI? = nil,
|
||||
userDefaults: UserDefaults = makeOTGTestDefaults()
|
||||
userDefaults: UserDefaults = makeOTGTestDefaults(),
|
||||
autoRetouchConfigurationStore: (any TravelAlbumAutoRetouchConfigurationStoring)? = nil,
|
||||
autoRetouchProcessor: (any TravelAlbumAutoRetouchProcessing)? = nil
|
||||
) -> WiredCameraTransferViewModel {
|
||||
prepareAppStoreForOTGTests()
|
||||
let configurationStore = autoRetouchConfigurationStore
|
||||
?? TravelAlbumAutoRetouchConfigurationStore(userDefaults: userDefaults)
|
||||
return WiredCameraTransferViewModel(
|
||||
albumId: albumId,
|
||||
albumTitle: "相册",
|
||||
@@ -1024,7 +1230,9 @@ private func makeWiredViewModel(
|
||||
storage: context.store,
|
||||
uploader: uploader ?? MockTravelAlbumOTGUploader(),
|
||||
api: api ?? TravelAlbumMockAPI(),
|
||||
userDefaults: userDefaults
|
||||
userDefaults: userDefaults,
|
||||
autoRetouchConfigurationStore: configurationStore,
|
||||
autoRetouchProcessor: autoRetouchProcessor
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1054,7 +1262,9 @@ private func makeOTGPhotoItem(
|
||||
progress: 0,
|
||||
errorMessage: nil,
|
||||
localPath: "originals/\(id).JPG",
|
||||
remoteUrl: ""
|
||||
remoteUrl: "",
|
||||
autoRetouchState: .none,
|
||||
autoRetouchTemplateId: nil
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1225,6 +1435,11 @@ private final class MockCameraDriver: CameraDriver {
|
||||
private final class MockTravelAlbumOTGUploader: TravelAlbumOTGUploading {
|
||||
private(set) var uploadCallCount = 0
|
||||
private(set) var uploadedRecordIds: [String] = []
|
||||
private let materialID: Int
|
||||
|
||||
init(materialID: Int = 1) {
|
||||
self.materialID = materialID
|
||||
}
|
||||
|
||||
func upload(
|
||||
record: TravelAlbumOTGPhotoRecord,
|
||||
@@ -1233,7 +1448,22 @@ private final class MockTravelAlbumOTGUploader: TravelAlbumOTGUploading {
|
||||
) async throws -> TravelAlbumMaterial {
|
||||
uploadCallCount += 1
|
||||
uploadedRecordIds.append(record.id)
|
||||
return TravelAlbumMaterial(id: 1, fileName: record.fileName, fileUrl: "https://cdn.example.com/\(record.fileName)")
|
||||
return TravelAlbumMaterial(id: materialID, fileName: record.fileName, fileUrl: "https://cdn.example.com/\(record.fileName)")
|
||||
}
|
||||
}
|
||||
|
||||
/// 自动修图测试替身,可切换失败以验证重试闭环。
|
||||
@MainActor
|
||||
private final class MockTravelAlbumAutoRetouchProcessor: TravelAlbumAutoRetouchProcessing {
|
||||
var shouldFail = false
|
||||
private(set) var processedPresetIDs: [String] = []
|
||||
|
||||
func process(sourceURL: URL, preset: TravelAlbumEditPreset) async throws -> Data {
|
||||
processedPresetIDs.append(preset.id)
|
||||
if shouldFail {
|
||||
throw TravelAlbumAutoRetouchError.resultEncodingFailed
|
||||
}
|
||||
return Data(base64Encoded: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl6rE8AAAAASUVORK5CYII=")!
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user