移除 Core 层 OTG 相机实现,保留旅拍有线传图 UI 与本地照片上传能力。

将本地存储与上传编排下沉到 Features/TravelAlbum,删除 USB/PTP 相关代码与测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 10:55:40 +08:00
parent a0549e068a
commit d7fec35715
42 changed files with 187 additions and 5278 deletions

View File

@ -0,0 +1,76 @@
//
// CameraDownloadStorageTests.swift
// suixinkanTests
//
import XCTest
@testable import suixinkan
/// CameraDownloadStorage
final class CameraDownloadStorageTests: XCTestCase {
/// scoped
func testRelativePathUsesScopedOriginalsPrefix() {
let scope = CameraDownloadStorage.Scope(accountKey: "user_101", albumID: 6603)
defer { CameraDownloadStorage.removeScopeDirectory(for: scope) }
let fileURL = CameraDownloadStorage.originalsDirectory(for: scope).appendingPathComponent("1730_test.JPG")
XCTAssertEqual(
CameraDownloadStorage.relativePath(for: fileURL),
"CameraDownloads/user_101/6603/originals/1730_test.JPG"
)
}
///
func testResolveLocalURLFromRelativePath() {
let stored = "CameraDownloads/user_101/6603/originals/1730_test.JPG"
let resolved = CameraDownloadStorage.resolveLocalURL(from: stored)
let expected = CameraDownloadStorage.documentsDirectory.appendingPathComponent(stored)
XCTAssertEqual(resolved, expected)
}
/// scoped
func testScopeDirectoryPath() {
let scope = CameraDownloadStorage.Scope(accountKey: "user_101", albumID: 6603)
defer { CameraDownloadStorage.removeScopeDirectory(for: scope) }
let directory = CameraDownloadStorage.scopeDirectory(for: scope)
XCTAssertTrue(directory.path.hasSuffix("CameraDownloads/user_101/6603"))
}
/// URL
func testUniqueLocalURLUsesScopedOriginalsDirectory() {
let scope = CameraDownloadStorage.Scope(accountKey: "user_101", albumID: 6603)
defer { CameraDownloadStorage.removeScopeDirectory(for: scope) }
let url = CameraDownloadStorage.uniqueLocalURL(for: "DSC.JPG", scope: scope)
XCTAssertTrue(url.path.contains("/CameraDownloads/user_101/6603/originals/"))
XCTAssertTrue(url.lastPathComponent.hasSuffix("_DSC.JPG"))
}
/// scoped
func testRemoveScopeDirectory() throws {
let scope = CameraDownloadStorage.Scope(accountKey: "user_101", albumID: 6603)
let fileURL = CameraDownloadStorage.uniqueLocalURL(for: "remove_test.JPG", scope: scope)
try Data(repeating: 0xFF, count: 8).write(to: fileURL)
let directory = CameraDownloadStorage.scopeDirectory(for: scope)
XCTAssertTrue(FileManager.default.fileExists(atPath: directory.path))
CameraDownloadStorage.removeScopeDirectory(for: scope)
XCTAssertFalse(FileManager.default.fileExists(atPath: directory.path))
}
/// URL
func testPreviewURLStringFromExistingFile() throws {
let scope = CameraDownloadStorage.Scope(accountKey: "user_101", albumID: 6603)
defer { CameraDownloadStorage.removeScopeDirectory(for: scope) }
let fileURL = CameraDownloadStorage.uniqueLocalURL(for: "preview.JPG", scope: scope)
try Data(repeating: 0xFF, count: 8).write(to: fileURL)
let stored = CameraDownloadStorage.relativePath(for: fileURL)
let preview = CameraDownloadStorage.previewURLString(from: stored)
XCTAssertFalse(preview.isEmpty)
XCTAssertTrue(preview.hasPrefix("file://"))
}
}

View File

@ -12,102 +12,15 @@ import UIKit
@MainActor
/// 线 ViewModel
final class WiredCameraTransferViewModelTests: XCTestCase {
///
func testFirstNewPipelineTaskAppearsInPhotos() async throws {
let camera = MockWiredCameraService()
/// start detach UI
func testStartCancellationDetachesFromUI() async {
let context = WiredTransferContext(
albumId: 6603,
albumName: "测试相册",
phone: "",
orderNumber: ""
)
let viewModel = WiredCameraTransferViewModel(context: context, cameraService: camera)
let account = AccountContext()
account.applyLogin(profile: AccountProfile(userId: "101", displayName: "测试"))
await viewModel.start(
api: MockTravelAlbumAPIForWiredTransfer(),
ossService: WiredTransferMockOSSUploadService(),
scenicID: 1,
accountContext: account
)
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("DSC_NEW.JPG")
try Data(repeating: 0xFF, count: 16).write(to: tempURL)
defer { try? FileManager.default.removeItem(at: tempURL) }
let asset = CameraAsset(id: "ptp_new_001", filename: "DSC_NEW.JPG", fileSize: 16)
camera.downloadURL = tempURL
camera.onNewAsset?(asset)
try await Task.sleep(nanoseconds: 500_000_000)
XCTAssertEqual(viewModel.photos.count, 1)
XCTAssertEqual(viewModel.photos.first?.id, "ptp_new_001")
XCTAssertEqual(viewModel.photos.first?.fileName, "DSC_NEW.JPG")
}
///
func testDeletedPendingPhotoDoesNotReappearAfterLiveCapture() async throws {
let camera = MockWiredCameraService()
let context = WiredTransferContext(
albumId: 7701,
albumName: "删除回归测试",
phone: "",
orderNumber: ""
)
let viewModel = WiredCameraTransferViewModel(context: context, cameraService: camera)
let account = AccountContext()
account.applyLogin(profile: AccountProfile(userId: "101", displayName: "测试"))
await viewModel.start(
api: MockTravelAlbumAPIForWiredTransfer(),
ossService: WiredTransferMockOSSUploadService(),
scenicID: 1,
accountContext: account
)
let oldURL = FileManager.default.temporaryDirectory.appendingPathComponent("OLD_DELETE.JPG")
try Data(repeating: 0x11, count: 16).write(to: oldURL)
defer { try? FileManager.default.removeItem(at: oldURL) }
camera.downloadURL = oldURL
camera.onNewAsset?(CameraAsset(id: "ptp_delete_me", filename: "OLD_DELETE.JPG", fileSize: 16))
try await Task.sleep(nanoseconds: 500_000_000)
XCTAssertEqual(viewModel.photos.count, 1)
viewModel.deletePhoto(id: "ptp_delete_me")
XCTAssertTrue(viewModel.photos.isEmpty)
viewModel.selectTransferMode(
WiredCameraTransferViewModel.modeLiveCapture,
api: MockTravelAlbumAPIForWiredTransfer(),
ossService: WiredTransferMockOSSUploadService(),
scenicID: 1
)
let newURL = FileManager.default.temporaryDirectory.appendingPathComponent("NEW_LIVE.JPG")
try Data(repeating: 0x22, count: 16).write(to: newURL)
defer { try? FileManager.default.removeItem(at: newURL) }
camera.downloadURL = newURL
camera.onNewAsset?(CameraAsset(id: "ptp_live_new", filename: "NEW_LIVE.JPG", fileSize: 16))
try await Task.sleep(nanoseconds: 500_000_000)
XCTAssertEqual(viewModel.photos.count, 1)
XCTAssertEqual(viewModel.photos.first?.id, "ptp_live_new")
XCTAssertFalse(viewModel.photos.contains { $0.id == "ptp_delete_me" })
}
/// start detach UI shutdown
func testStartCancellationDetachesWithoutShutdown() async {
let camera = DisconnectTrackingWiredCameraService()
let context = WiredTransferContext(
albumId: 6603,
albumName: "测试相册",
phone: "",
orderNumber: ""
)
let viewModel = WiredCameraTransferViewModel(context: context, cameraService: camera)
let viewModel = WiredCameraTransferViewModel(context: context)
let account = AccountContext()
account.applyLogin(profile: AccountProfile(userId: "101", displayName: "测试"))
@ -120,54 +33,11 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
)
}
for _ in 0 ..< 100 {
if camera.connectCount > 0 { break }
try? await Task.sleep(nanoseconds: 10_000_000)
}
try? await Task.sleep(nanoseconds: 100_000_000)
startTask.cancel()
try? await Task.sleep(nanoseconds: 300_000_000)
try? await Task.sleep(nanoseconds: 200_000_000)
XCTAssertGreaterThanOrEqual(camera.detachCount, 1)
XCTAssertEqual(camera.shutdownCount, 0)
}
///
func testPhotoBoundToOtherAlbumIsFilteredOut() async throws {
let bindingStore = WiredTransferPhotoStore(
accountPrefixProvider: { "user_101" },
userIDProvider: { "101" }
)
bindingStore.bindPhotoToAlbum(photoID: "ptp_other_album", albumID: 99)
defer { bindingStore.bindPhotoToAlbum(photoID: "ptp_other_album", albumID: 1) }
let camera = MockWiredCameraService()
let context = WiredTransferContext(
albumId: 1,
albumName: "当前相册",
phone: "",
orderNumber: ""
)
let viewModel = WiredCameraTransferViewModel(context: context, cameraService: camera)
let account = AccountContext()
account.applyLogin(profile: AccountProfile(userId: "101", displayName: "测试"), accountType: "user")
await viewModel.start(
api: MockTravelAlbumAPIForWiredTransfer(),
ossService: WiredTransferMockOSSUploadService(),
scenicID: 1,
accountContext: account
)
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("DSC_OTHER.JPG")
try Data(repeating: 0xAA, count: 8).write(to: tempURL)
defer { try? FileManager.default.removeItem(at: tempURL) }
camera.downloadURL = tempURL
camera.onNewAsset?(CameraAsset(id: "ptp_other_album", filename: "DSC_OTHER.JPG", fileSize: 8))
try await Task.sleep(nanoseconds: 500_000_000)
XCTAssertTrue(viewModel.photos.isEmpty)
XCTAssertEqual(viewModel.connectionState, .disconnected)
}
///
@ -209,8 +79,7 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
let today = formatter.string(from: Date())
let viewModel = WiredCameraTransferViewModel(
context: WiredTransferContext(albumId: 1, albumName: "相册", phone: "", orderNumber: ""),
cameraService: MockWiredCameraService()
context: WiredTransferContext(albumId: 1, albumName: "相册", phone: "", orderNumber: "")
)
viewModel.replacePhotos([
makePendingPhoto(id: "today", capturedAt: today),
@ -226,111 +95,8 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
XCTAssertNil(viewModel.errorMessage)
}
/// capturedAt
func testPipelineProgressUpdatePreservesCapturedAt() async throws {
let camera = MockWiredCameraService()
let context = WiredTransferContext(
albumId: 1,
albumName: "测试相册",
phone: "",
orderNumber: ""
)
let viewModel = WiredCameraTransferViewModel(context: context, cameraService: camera)
let account = AccountContext()
account.applyLogin(profile: AccountProfile(userId: "101", displayName: "测试"))
await viewModel.start(
api: MockTravelAlbumAPIForWiredTransfer(),
ossService: WiredTransferMockOSSUploadService(),
scenicID: 1,
accountContext: account
)
let originalCapturedAt = "2026-05-20 12:05:18"
viewModel.replacePhotos([
WiredTransferPhotoItem(
id: "ptp_progress",
fileName: "DSC_PROGRESS.JPG",
thumbnailURL: "",
previewURL: "",
capturedAt: originalCapturedAt,
fileSizeText: "2 MB",
status: .uploading,
progress: 10
),
])
let scope = downloadScope(account: account, albumID: context.albumId)
let fileURL = CameraDownloadStorage.uniqueLocalURL(for: "DSC_PROGRESS.JPG", scope: scope)
try Data(repeating: 0xAB, count: 16).write(to: fileURL)
defer { CameraDownloadStorage.removeScopeDirectory(for: scope) }
camera.downloadURL = fileURL
camera.onNewAsset?(CameraAsset(id: "ptp_progress", filename: "DSC_PROGRESS.JPG", fileSize: 16))
try await Task.sleep(nanoseconds: 300_000_000)
XCTAssertEqual(viewModel.photos.first?.capturedAt, originalCapturedAt)
}
///
func testNewerCapturedPhotoAppearsFirst() async throws {
let camera = MockWiredCameraService()
let context = WiredTransferContext(
albumId: 6604,
albumName: "测试相册",
phone: "",
orderNumber: ""
)
let viewModel = WiredCameraTransferViewModel(context: context, cameraService: camera)
let account = AccountContext()
account.applyLogin(profile: AccountProfile(userId: "101", displayName: "测试"))
await viewModel.start(
api: MockTravelAlbumAPIForWiredTransfer(),
ossService: WiredTransferMockOSSUploadService(),
scenicID: 1,
accountContext: account
)
let fileURL = FileManager.default.temporaryDirectory
let olderURL = fileURL.appendingPathComponent("OLDER.JPG")
let newerURL = fileURL.appendingPathComponent("NEWER.JPG")
try Data(repeating: 0xAA, count: 8).write(to: olderURL)
try Data(repeating: 0xBB, count: 8).write(to: newerURL)
defer {
try? FileManager.default.removeItem(at: olderURL)
try? FileManager.default.removeItem(at: newerURL)
}
camera.downloadURL = olderURL
camera.onNewAsset?(
CameraAsset(
id: "ptp_old",
filename: "OLDER.JPG",
fileSize: 8,
creationDate: Date(timeIntervalSince1970: 1_700_000_000)
)
)
try await Task.sleep(nanoseconds: 400_000_000)
camera.downloadURL = newerURL
camera.onNewAsset?(
CameraAsset(
id: "ptp_new",
filename: "NEWER.JPG",
fileSize: 8,
creationDate: Date(timeIntervalSince1970: 1_800_000_000)
)
)
try await Task.sleep(nanoseconds: 400_000_000)
XCTAssertEqual(viewModel.photos.count, 2)
XCTAssertEqual(viewModel.photos[0].id, "ptp_new")
XCTAssertEqual(viewModel.photos[1].id, "ptp_old")
}
///
func testRetryPhotoInPostShootModeUploadsPersistedFile() async throws {
let camera = MockWiredCameraService()
let context = WiredTransferContext(
albumId: 1,
albumName: "测试相册",
@ -368,7 +134,7 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
)
store.save(albumID: 1, records: [record])
let viewModel = WiredCameraTransferViewModel(context: context, cameraService: camera)
let viewModel = WiredCameraTransferViewModel(context: context)
viewModel.transferModeOption = WiredCameraTransferViewModel.modePostShootTransfer
let api = MockTravelAlbumAPIForWiredTransfer()
@ -418,8 +184,7 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
store.save(albumID: albumID, records: records)
let viewModel = WiredCameraTransferViewModel(
context: WiredTransferContext(albumId: albumID, albumName: "批量上传", phone: "", orderNumber: ""),
cameraService: MockWiredCameraService()
context: WiredTransferContext(albumId: albumID, albumName: "批量上传", phone: "", orderNumber: "")
)
let oss = WiredTransferMockOSSUploadService()
oss.uploadDelayNanoseconds = 150_000_000
@ -447,32 +212,36 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
///
func testSelectTransferModePersistsOption() async {
let account = makeTransferModeTestAccount()
let context = WiredTransferContext(albumId: 7701, albumName: "模式缓存", phone: "", orderNumber: "")
let viewModel = WiredCameraTransferViewModel(context: context, cameraService: MockWiredCameraService())
let api = MockTravelAlbumAPIForWiredTransfer()
let oss = WiredTransferMockOSSUploadService()
let store = makeTransferModeTestStore(account: account)
let viewModel = WiredCameraTransferViewModel(
context: WiredTransferContext(albumId: 1, albumName: "相册", phone: "", orderNumber: "")
)
await viewModel.start(
api: MockTravelAlbumAPIForWiredTransfer(),
ossService: WiredTransferMockOSSUploadService(),
scenicID: 1,
accountContext: account
)
await viewModel.start(api: api, ossService: oss, scenicID: 1, accountContext: account)
viewModel.selectTransferMode(
WiredCameraTransferViewModel.modePostShootTransfer,
api: api,
ossService: oss,
api: MockTravelAlbumAPIForWiredTransfer(),
ossService: WiredTransferMockOSSUploadService(),
scenicID: 1
)
let store = makeTransferModeTestStore(account: account)
XCTAssertEqual(store.loadTransferModeOption(), WiredCameraTransferViewModel.modePostShootTransfer)
}
/// 线
///
func testStartRestoresCachedTransferModeOption() async {
let account = makeTransferModeTestAccount()
let store = makeTransferModeTestStore(account: account)
store.saveTransferModeOption(WiredCameraTransferViewModel.modePostShootTransfer)
let context = WiredTransferContext(albumId: 7702, albumName: "模式恢复", phone: "", orderNumber: "")
let viewModel = WiredCameraTransferViewModel(context: context, cameraService: MockWiredCameraService())
let viewModel = WiredCameraTransferViewModel(
context: WiredTransferContext(albumId: 1, albumName: "相册", phone: "", orderNumber: "")
)
await viewModel.start(
api: MockTravelAlbumAPIForWiredTransfer(),
ossService: WiredTransferMockOSSUploadService(),
@ -483,37 +252,6 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
XCTAssertEqual(viewModel.transferModeOption, WiredCameraTransferViewModel.modePostShootTransfer)
}
///
func testDownloadedPhotoGeneratesThumbnailForList() async throws {
let camera = MockWiredCameraService()
let albumID = 6601
let context = WiredTransferContext(albumId: albumID, albumName: "缩略图相册", phone: "", orderNumber: "")
let viewModel = WiredCameraTransferViewModel(context: context, cameraService: camera)
let account = AccountContext()
account.applyLogin(profile: AccountProfile(userId: "thumb-user", displayName: "测试"))
await viewModel.start(
api: MockTravelAlbumAPIForWiredTransfer(),
ossService: WiredTransferMockOSSUploadService(),
scenicID: 1,
accountContext: account
)
let scope = downloadScope(account: account, albumID: albumID)
let imageURL = CameraDownloadStorage.uniqueLocalURL(for: "thumb_source.JPG", scope: scope)
try makeJPEGData().write(to: imageURL)
defer { CameraDownloadStorage.removeScopeDirectory(for: scope) }
camera.downloadURL = imageURL
camera.onNewAsset?(CameraAsset(id: "thumb_asset_001", filename: "thumb_source.JPG", fileSize: 128))
try await Task.sleep(nanoseconds: 900_000_000)
let photo = try XCTUnwrap(viewModel.photos.first { $0.id == "thumb_asset_001" })
XCTAssertTrue(photo.thumbnailURL.contains("/CameraDownloads/\(scope.accountDirectoryName)/\(albumID)/thumbnails/"))
XCTAssertTrue(photo.previewURL.contains("/CameraDownloads/"))
XCTAssertFalse(photo.previewURL.contains("/thumbnails/"))
}
///
func testLegacyRecordWithoutThumbnailKeepsPreviewURL() async throws {
let albumID = 6602
@ -547,8 +285,7 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
store.save(albumID: albumID, records: [record])
let viewModel = WiredCameraTransferViewModel(
context: WiredTransferContext(albumId: albumID, albumName: "旧记录", phone: "", orderNumber: ""),
cameraService: MockWiredCameraService()
context: WiredTransferContext(albumId: albumID, albumName: "旧记录", phone: "", orderNumber: "")
)
await viewModel.start(
api: MockTravelAlbumAPIForWiredTransfer(),
@ -578,10 +315,10 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
CameraDownloadStorage.removeScopeDirectory(for: otherScope)
}
let fileURL = CameraDownloadStorage.uniqueLocalURL(for: "clear_target.JPG", scope: scope)
let otherFileURL = CameraDownloadStorage.uniqueLocalURL(for: "clear_other.JPG", scope: otherScope)
try Data(repeating: 0x01, count: 8).write(to: fileURL)
try Data(repeating: 0x02, count: 8).write(to: otherFileURL)
let storeURL = CameraDownloadStorage.uniqueLocalURL(for: "store_photo.JPG", scope: scope)
let otherURL = CameraDownloadStorage.uniqueLocalURL(for: "other_photo.JPG", scope: otherScope)
try makeJPEGData().write(to: storeURL)
try makeJPEGData().write(to: otherURL)
let store = WiredTransferPhotoStore(
accountPrefixProvider: { account.accountCachePrefix ?? "guest_" },
@ -589,40 +326,29 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
)
store.save(albumID: albumID, records: [
makePhotoRecord(
id: "clear_target",
fileName: "clear_target.JPG",
localPath: CameraDownloadStorage.relativePath(for: fileURL),
id: "store_photo",
fileName: "store_photo.JPG",
localPath: CameraDownloadStorage.relativePath(for: storeURL),
albumID: albumID,
userID: userID
)
])
store.save(albumID: otherAlbumID, records: [
makePhotoRecord(
id: "clear_other",
fileName: "clear_other.JPG",
localPath: CameraDownloadStorage.relativePath(for: otherFileURL),
albumID: otherAlbumID,
userID: userID
)
])
store.clearAlbum(albumID: albumID)
let deletedIDs = store.clearAlbum(albumID: albumID)
XCTAssertEqual(deletedIDs, Set(["clear_target"]))
XCTAssertFalse(FileManager.default.fileExists(atPath: fileURL.path))
XCTAssertFalse(FileManager.default.fileExists(atPath: scopeDirectoryPath))
XCTAssertTrue(FileManager.default.fileExists(atPath: otherFileURL.path))
XCTAssertTrue(FileManager.default.fileExists(atPath: otherScopeDirectoryPath))
}
/// ID 线
///
func testSameUserIDDifferentAccountTypesAreIsolated() throws {
let userID = "multi-account-user"
let albumID = 8803
let userID = "same-user-id"
let albumID = 9901
let storeAccount = AccountContext()
storeAccount.applyLogin(profile: AccountProfile(userId: userID, displayName: "门店"), accountType: "store")
let photographerAccount = AccountContext()
photographerAccount.applyLogin(profile: AccountProfile(userId: userID, displayName: "摄影师"), accountType: "photographer")
let storeScope = downloadScope(account: storeAccount, albumID: albumID)
let photographerScope = downloadScope(account: photographerAccount, albumID: albumID)
defer {
@ -632,10 +358,10 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
let storeURL = CameraDownloadStorage.uniqueLocalURL(for: "store_photo.JPG", scope: storeScope)
let photographerURL = CameraDownloadStorage.uniqueLocalURL(for: "photographer_photo.JPG", scope: photographerScope)
try Data(repeating: 0x03, count: 8).write(to: storeURL)
try Data(repeating: 0x04, count: 8).write(to: photographerURL)
try makeJPEGData().write(to: storeURL)
try makeJPEGData().write(to: photographerURL)
let store = WiredTransferPhotoStore(
let storeStore = WiredTransferPhotoStore(
accountPrefixProvider: { storeAccount.accountCachePrefix ?? "guest_" },
userIDProvider: { userID }
)
@ -643,7 +369,7 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
accountPrefixProvider: { photographerAccount.accountCachePrefix ?? "guest_" },
userIDProvider: { userID }
)
store.save(albumID: albumID, records: [
storeStore.save(albumID: albumID, records: [
makePhotoRecord(
id: "store_photo",
fileName: "store_photo.JPG",
@ -662,7 +388,7 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
)
])
XCTAssertEqual(store.load(albumID: albumID).map(\.id), ["store_photo"])
XCTAssertEqual(storeStore.load(albumID: albumID).map(\.id), ["store_photo"])
XCTAssertEqual(photographerStore.load(albumID: albumID).map(\.id), ["photographer_photo"])
XCTAssertNotEqual(
CameraDownloadStorage.scopeDirectory(for: storeScope).path,
@ -672,8 +398,7 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
private func makeViewModelWithPendingPhotos() -> WiredCameraTransferViewModel {
let viewModel = WiredCameraTransferViewModel(
context: WiredTransferContext(albumId: 1, albumName: "相册", phone: "", orderNumber: ""),
cameraService: MockWiredCameraService()
context: WiredTransferContext(albumId: 1, albumName: "相册", phone: "", orderNumber: "")
)
viewModel.replacePhotos([
makePendingPhoto(id: "pending-1", capturedAt: "2026-05-20 12:05:18"),
@ -741,63 +466,6 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
}
}
@MainActor
private final class DisconnectTrackingWiredCameraService: CameraServiceProtocol {
let brand: CameraBrand = .sony
var connectionState: CameraConnectionState = .disconnected
var onConnectionStateChange: ((CameraConnectionState) -> Void)?
var onNewAsset: ((CameraAsset) -> Void)?
private(set) var connectCount = 0
private(set) var detachCount = 0
private(set) var shutdownCount = 0
func connect() async {
connectCount += 1
connectionState = .connected(deviceName: "Mock")
onConnectionStateChange?(.connected(deviceName: "Mock"))
while !Task.isCancelled {
try? await Task.sleep(nanoseconds: 50_000_000)
}
}
func detachFromUI() { detachCount += 1 }
func shutdown() async { shutdownCount += 1 }
func listAssets() async throws -> [CameraAsset] { [] }
func downloadAsset(_ asset: CameraAsset) async throws -> URL {
FileManager.default.temporaryDirectory.appendingPathComponent(asset.filename)
}
}
@MainActor
private final class MockWiredCameraService: CameraServiceProtocol {
let brand: CameraBrand = .sony
var connectionState: CameraConnectionState = .connected(deviceName: "Mock")
var onConnectionStateChange: ((CameraConnectionState) -> Void)?
var onNewAsset: ((CameraAsset) -> Void)?
var downloadURL: URL?
func connect() async {}
func detachFromUI() {}
func shutdown() async {}
func listAssets() async throws -> [CameraAsset] {
[]
}
func downloadAsset(_ asset: CameraAsset) async throws -> URL {
if let downloadURL {
return downloadURL
}
let url = FileManager.default.temporaryDirectory.appendingPathComponent(asset.filename)
if !FileManager.default.fileExists(atPath: url.path) {
try Data(repeating: 0xFF, count: 8).write(to: url)
}
return url
}
}
@MainActor
private final class MockTravelAlbumAPIForWiredTransfer: TravelAlbumServing {
func availableOrders() async throws -> [TravelAlbumAvailableOrder] { [] }