将 otg_swift OTG 引擎迁入 Core/CameraOTG,并接入旅拍有线传图与历史导入。
恢复 USB 相机连接、边拍边传、三品牌驱动与 SwiftUI 历史导入页,通过适配层对接现有本地上传与 OSS 管道。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
82
suixinkanTests/CameraOTG/OTGPhotoStorageTests.swift
Normal file
82
suixinkanTests/CameraOTG/OTGPhotoStorageTests.swift
Normal file
@ -0,0 +1,82 @@
|
||||
//
|
||||
// OTGPhotoStorageTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// OTGPhotoFileWriter scoped 路径写入与去重测试。
|
||||
final class OTGPhotoStorageTests: XCTestCase {
|
||||
private let accountKey = "test_account"
|
||||
private let albumID = 12345
|
||||
|
||||
override func tearDown() {
|
||||
let directory = OTGPhotoFileWriter.originalsDirectory(accountKey: accountKey, albumID: albumID)
|
||||
try? FileManager.default.removeItem(at: directory.deletingLastPathComponent())
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
/// 测试 scoped 原图目录按账号与相册隔离。
|
||||
func testOriginalsDirectoryScopedByAccountAndAlbum() {
|
||||
let directory = OTGPhotoFileWriter.originalsDirectory(accountKey: accountKey, albumID: albumID)
|
||||
XCTAssertTrue(directory.path.contains("CameraDownloads"))
|
||||
XCTAssertTrue(directory.path.contains("\(albumID)"))
|
||||
XCTAssertTrue(directory.lastPathComponent == "originals")
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: directory.path))
|
||||
}
|
||||
|
||||
/// 测试 writeImage 落盘并返回相对路径。
|
||||
func testWriteImageReturnsRelativePath() throws {
|
||||
let data = Data(repeating: 0xAB, count: 64)
|
||||
let fileURL = try OTGPhotoFileWriter.writeImage(
|
||||
data,
|
||||
filename: "DSC_0001.JPG",
|
||||
accountKey: accountKey,
|
||||
albumID: albumID
|
||||
)
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: fileURL.path))
|
||||
|
||||
let relativePath = OTGPhotoFileWriter.relativePath(for: fileURL)
|
||||
XCTAssertFalse(relativePath.hasPrefix("/"))
|
||||
XCTAssertTrue(relativePath.contains("DSC_0001.JPG"))
|
||||
|
||||
let resolved = CameraDownloadStorage.resolveLocalURL(from: relativePath)
|
||||
XCTAssertEqual(resolved?.standardizedFileURL, fileURL.standardizedFileURL)
|
||||
}
|
||||
|
||||
/// 测试同名文件自动追加序号避免覆盖。
|
||||
func testUniqueFileURLAppendsCounterForDuplicates() throws {
|
||||
let data = Data(repeating: 0xCD, count: 16)
|
||||
let firstURL = try OTGPhotoFileWriter.writeImage(
|
||||
data,
|
||||
filename: "duplicate.JPG",
|
||||
accountKey: accountKey,
|
||||
albumID: albumID
|
||||
)
|
||||
let secondURL = try OTGPhotoFileWriter.uniqueFileURL(
|
||||
filename: "duplicate.JPG",
|
||||
accountKey: accountKey,
|
||||
albumID: albumID
|
||||
)
|
||||
|
||||
XCTAssertNotEqual(firstURL.lastPathComponent, secondURL.lastPathComponent)
|
||||
XCTAssertTrue(secondURL.lastPathComponent.contains("duplicate_1"))
|
||||
}
|
||||
|
||||
/// 测试 moveDownloadedFile 将临时文件移入 scoped 目录。
|
||||
func testMoveDownloadedFileIntoScopedDirectory() throws {
|
||||
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("otg-move-test.JPG")
|
||||
try Data(repeating: 0xEF, count: 32).write(to: tempURL)
|
||||
defer { try? FileManager.default.removeItem(at: tempURL) }
|
||||
|
||||
let destination = try OTGPhotoFileWriter.moveDownloadedFile(
|
||||
from: tempURL,
|
||||
filename: "imported.JPG",
|
||||
accountKey: accountKey,
|
||||
albumID: albumID
|
||||
)
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: destination.path))
|
||||
XCTAssertFalse(FileManager.default.fileExists(atPath: tempURL.path))
|
||||
}
|
||||
}
|
||||
76
suixinkanTests/CameraOTG/PTPUtilitiesTests.swift
Normal file
76
suixinkanTests/CameraOTG/PTPUtilitiesTests.swift
Normal file
@ -0,0 +1,76 @@
|
||||
//
|
||||
// PTPUtilitiesTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// PTP Container 构建与解析测试。
|
||||
final class PTPUtilitiesTests: XCTestCase {
|
||||
/// 测试 buildCommand 与 parseContainer 往返一致。
|
||||
func testBuildAndParseCommandRoundTrip() throws {
|
||||
let command = PTPHelper.buildCommand(
|
||||
code: PTPStandardCommand.getObject,
|
||||
transactionID: 42,
|
||||
parameters: [1, 2, 3]
|
||||
)
|
||||
|
||||
let parsed = try PTPHelper.parseContainer(command)
|
||||
XCTAssertEqual(parsed.length, UInt32(12 + 4 * 3))
|
||||
XCTAssertEqual(parsed.type, PTPContainerType.command.rawValue)
|
||||
XCTAssertEqual(parsed.code, PTPStandardCommand.getObject)
|
||||
XCTAssertEqual(parsed.transactionID, 42)
|
||||
XCTAssertEqual(parsed.params, [1, 2, 3])
|
||||
}
|
||||
|
||||
/// 测试数据过短时抛出 tooShort。
|
||||
func testParseContainerTooShortThrows() {
|
||||
XCTAssertThrowsError(try PTPHelper.parseContainer(Data([0x01, 0x02]))) { error in
|
||||
XCTAssertEqual(error as? PTPParseError, .tooShort)
|
||||
}
|
||||
}
|
||||
|
||||
/// 测试 length 与 buffer 不一致时抛出 invalidLength。
|
||||
func testParseContainerInvalidLengthThrows() {
|
||||
var data = Data(count: 12)
|
||||
data[0] = 32
|
||||
XCTAssertThrowsError(try PTPHelper.parseContainer(data)) { error in
|
||||
XCTAssertEqual(error as? PTPParseError, .invalidLength)
|
||||
}
|
||||
}
|
||||
|
||||
/// 测试 objectSignature 拼接规则。
|
||||
func testObjectSignature() {
|
||||
XCTAssertEqual(PTPHelper.objectSignature(filename: "DSC_0001.JPG", size: 2048), "DSC_0001.JPG_2048")
|
||||
}
|
||||
|
||||
/// 测试 sanitizeFilename 去除非法路径字符。
|
||||
func testSanitizeFilename() {
|
||||
XCTAssertEqual(PTPHelper.sanitizeFilename("DSC/0001.JPG"), "DSC_0001.JPG")
|
||||
XCTAssertEqual(PTPHelper.sanitizeFilename(" "), "photo.jpg")
|
||||
}
|
||||
|
||||
/// 测试从 ObjectInfo 固定偏移解析压缩大小。
|
||||
func testParseObjectCompressedSize() {
|
||||
var data = Data(count: 12)
|
||||
data[8] = 0x00
|
||||
data[9] = 0x10
|
||||
data[10] = 0x00
|
||||
data[11] = 0x00
|
||||
XCTAssertEqual(PTPHelper.parseObjectCompressedSize(data), 4096)
|
||||
}
|
||||
|
||||
/// 测试 ResponseOK 判断。
|
||||
func testIsResponseOK() throws {
|
||||
let okResponse = PTPHelper.buildCommand(
|
||||
code: PTPStandardCommand.responseOK,
|
||||
transactionID: 1,
|
||||
parameters: []
|
||||
)
|
||||
var responseData = okResponse
|
||||
responseData[4] = UInt8(PTPContainerType.response.rawValue & 0xFF)
|
||||
responseData[5] = UInt8((PTPContainerType.response.rawValue >> 8) & 0xFF)
|
||||
XCTAssertTrue(PTPHelper.isResponseOK(responseData))
|
||||
}
|
||||
}
|
||||
95
suixinkanTests/CameraOTG/PlatformDetectorTests.swift
Normal file
95
suixinkanTests/CameraOTG/PlatformDetectorTests.swift
Normal file
@ -0,0 +1,95 @@
|
||||
//
|
||||
// PlatformDetectorTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// PlatformDetector USB VID 与型号关键字识别测试。
|
||||
final class PlatformDetectorTests: XCTestCase {
|
||||
/// 测试 Sony USB Vendor ID 识别。
|
||||
func testDetectSonyByVendorID() {
|
||||
let info = CameraDeviceInfo(
|
||||
name: "USB Device",
|
||||
manufacturer: nil,
|
||||
model: nil,
|
||||
serialNumber: nil,
|
||||
usbVendorID: 0x054C,
|
||||
usbProductID: 0x1234
|
||||
)
|
||||
XCTAssertEqual(PlatformDetector.detect(from: info), .sony)
|
||||
}
|
||||
|
||||
/// 测试 Canon USB Vendor ID 识别。
|
||||
func testDetectCanonByVendorID() {
|
||||
let info = CameraDeviceInfo(
|
||||
name: "USB Device",
|
||||
manufacturer: nil,
|
||||
model: nil,
|
||||
serialNumber: nil,
|
||||
usbVendorID: 0x04A9,
|
||||
usbProductID: nil
|
||||
)
|
||||
XCTAssertEqual(PlatformDetector.detect(from: info), .canon)
|
||||
}
|
||||
|
||||
/// 测试 Nikon USB Vendor ID 识别。
|
||||
func testDetectNikonByVendorID() {
|
||||
let info = CameraDeviceInfo(
|
||||
name: "USB Device",
|
||||
manufacturer: nil,
|
||||
model: nil,
|
||||
serialNumber: nil,
|
||||
usbVendorID: 0x04B0,
|
||||
usbProductID: nil
|
||||
)
|
||||
XCTAssertEqual(PlatformDetector.detect(from: info), .nikon)
|
||||
}
|
||||
|
||||
/// 测试 VID 不可用时按设备名关键字兜底识别。
|
||||
func testDetectByDeviceNameKeyword() {
|
||||
let sony = CameraDeviceInfo(
|
||||
name: "ILCE-7M4",
|
||||
manufacturer: nil,
|
||||
model: nil,
|
||||
serialNumber: nil,
|
||||
usbVendorID: nil,
|
||||
usbProductID: nil
|
||||
)
|
||||
XCTAssertEqual(PlatformDetector.detect(from: sony), .sony)
|
||||
|
||||
let canon = CameraDeviceInfo(
|
||||
name: "Canon EOS R5",
|
||||
manufacturer: nil,
|
||||
model: nil,
|
||||
serialNumber: nil,
|
||||
usbVendorID: nil,
|
||||
usbProductID: nil
|
||||
)
|
||||
XCTAssertEqual(PlatformDetector.detect(from: canon), .canon)
|
||||
|
||||
let nikon = CameraDeviceInfo(
|
||||
name: "Nikon Z 8",
|
||||
manufacturer: nil,
|
||||
model: nil,
|
||||
serialNumber: nil,
|
||||
usbVendorID: nil,
|
||||
usbProductID: nil
|
||||
)
|
||||
XCTAssertEqual(PlatformDetector.detect(from: nikon), .nikon)
|
||||
}
|
||||
|
||||
/// 测试无法识别时返回 unknown。
|
||||
func testDetectUnknownWhenNoMatch() {
|
||||
let info = CameraDeviceInfo(
|
||||
name: "Generic Webcam",
|
||||
manufacturer: nil,
|
||||
model: nil,
|
||||
serialNumber: nil,
|
||||
usbVendorID: nil,
|
||||
usbProductID: nil
|
||||
)
|
||||
XCTAssertEqual(PlatformDetector.detect(from: info), .unknown)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,224 @@
|
||||
//
|
||||
// CameraHistoryImportViewModelTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
@MainActor
|
||||
/// 相机历史导入 ViewModel 测试。
|
||||
final class CameraHistoryImportViewModelTests: XCTestCase {
|
||||
/// 测试按日分组与今天/昨天标题。
|
||||
func testLoadPhotosGroupsByDay() async throws {
|
||||
let calendar = Calendar.current
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
let yesterday = calendar.date(byAdding: .day, value: -1, to: today)!
|
||||
|
||||
let driver = MockHistoryImportCameraDriver()
|
||||
driver.objects = [
|
||||
makeObject(id: "today-1", filename: "today1.JPG", day: today, hour: 10),
|
||||
makeObject(id: "today-2", filename: "today2.JPG", day: today, hour: 9),
|
||||
makeObject(id: "yesterday-1", filename: "yesterday1.JPG", day: yesterday, hour: 15),
|
||||
]
|
||||
|
||||
let sink = MockHistoryImportPhotoSink()
|
||||
let viewModel = CameraHistoryImportViewModel(
|
||||
albumId: 1,
|
||||
accountKey: "test",
|
||||
driver: driver,
|
||||
photoSink: sink,
|
||||
onImported: {}
|
||||
)
|
||||
|
||||
viewModel.loadPhotos()
|
||||
try await waitUntilLoaded(viewModel)
|
||||
|
||||
guard case .loaded = viewModel.state else {
|
||||
return XCTFail("expected loaded state, got \(viewModel.state)")
|
||||
}
|
||||
XCTAssertEqual(viewModel.sections.count, 2)
|
||||
XCTAssertEqual(viewModel.sections[0].title, "今天")
|
||||
XCTAssertEqual(viewModel.sections[0].photos.map(\.id), ["today-1", "today-2"])
|
||||
XCTAssertEqual(viewModel.sections[1].title, "昨天")
|
||||
}
|
||||
|
||||
/// 测试已导入照片不可选。
|
||||
func testAlreadyImportedPhotoIsNotSelectable() async throws {
|
||||
let driver = MockHistoryImportCameraDriver()
|
||||
driver.objects = [makeObject(id: "dup|1024", filename: "dup.JPG", day: Date(), hour: 12)]
|
||||
|
||||
let sink = MockHistoryImportPhotoSink()
|
||||
sink.existingIDs = ["dup|1024"]
|
||||
|
||||
let viewModel = CameraHistoryImportViewModel(
|
||||
albumId: 1,
|
||||
accountKey: "test",
|
||||
driver: driver,
|
||||
photoSink: sink,
|
||||
onImported: {}
|
||||
)
|
||||
|
||||
viewModel.loadPhotos()
|
||||
try await waitUntilLoaded(viewModel)
|
||||
|
||||
XCTAssertTrue(viewModel.isPhotoAlreadyInAlbum(id: "dup|1024"))
|
||||
viewModel.togglePhoto(id: "dup|1024")
|
||||
XCTAssertTrue(viewModel.selectedPhotoIDs.isEmpty)
|
||||
}
|
||||
|
||||
/// 测试分组全选与取消全选。
|
||||
func testToggleSectionSelectsImportablePhotosOnly() async throws {
|
||||
let day = Calendar.current.startOfDay(for: Date())
|
||||
let driver = MockHistoryImportCameraDriver()
|
||||
driver.objects = [
|
||||
makeObject(id: "a|100", filename: "a.JPG", day: day, hour: 10),
|
||||
makeObject(id: "b|200", filename: "b.JPG", day: day, hour: 9),
|
||||
]
|
||||
|
||||
let sink = MockHistoryImportPhotoSink()
|
||||
sink.existingIDs = ["b|200"]
|
||||
|
||||
let viewModel = CameraHistoryImportViewModel(
|
||||
albumId: 1,
|
||||
accountKey: "test",
|
||||
driver: driver,
|
||||
photoSink: sink,
|
||||
onImported: {}
|
||||
)
|
||||
|
||||
viewModel.loadPhotos()
|
||||
try await waitUntilLoaded(viewModel)
|
||||
|
||||
let section = try XCTUnwrap(viewModel.sections.first)
|
||||
viewModel.toggleSection(section)
|
||||
XCTAssertEqual(viewModel.selectedPhotoIDs, ["a|100"])
|
||||
|
||||
viewModel.toggleSection(section)
|
||||
XCTAssertTrue(viewModel.selectedPhotoIDs.isEmpty)
|
||||
}
|
||||
|
||||
/// 测试 Sony catalog 为空时显示 MTP 帮助入口。
|
||||
func testSonyEmptyCatalogShowsMTPHelp() async throws {
|
||||
let driver = MockHistoryImportCameraDriver()
|
||||
driver.platform = .sony
|
||||
driver.objects = []
|
||||
|
||||
let viewModel = CameraHistoryImportViewModel(
|
||||
albumId: 1,
|
||||
accountKey: "test",
|
||||
driver: driver,
|
||||
photoSink: MockHistoryImportPhotoSink(),
|
||||
onImported: {}
|
||||
)
|
||||
|
||||
viewModel.loadPhotos()
|
||||
try await waitForFailedState(viewModel)
|
||||
|
||||
XCTAssertTrue(viewModel.shouldShowSonyMTPHelp)
|
||||
}
|
||||
|
||||
private func makeObject(id: String, filename: String, day: Date, hour: Int) -> CameraObject {
|
||||
let capturedAt = Calendar.current.date(bySettingHour: hour, minute: 0, second: 0, of: day) ?? day
|
||||
return CameraObject(id: id, filename: filename, fileSize: 1024, capturedAt: capturedAt)
|
||||
}
|
||||
|
||||
private func waitUntilLoaded(
|
||||
_ viewModel: CameraHistoryImportViewModel,
|
||||
timeout: TimeInterval = 2
|
||||
) async throws {
|
||||
let deadline = Date().addingTimeInterval(timeout)
|
||||
while Date() < deadline {
|
||||
if case .loaded = viewModel.state { return }
|
||||
if case .failed = viewModel.state {
|
||||
XCTFail("unexpected failed: \(viewModel.state)")
|
||||
return
|
||||
}
|
||||
try await Task.sleep(nanoseconds: 50_000_000)
|
||||
}
|
||||
XCTFail("timed out waiting for loaded state")
|
||||
}
|
||||
|
||||
private func waitForFailedState(
|
||||
_ viewModel: CameraHistoryImportViewModel,
|
||||
timeout: TimeInterval = 2
|
||||
) async throws {
|
||||
let deadline = Date().addingTimeInterval(timeout)
|
||||
while Date() < deadline {
|
||||
if case .failed = viewModel.state { return }
|
||||
try await Task.sleep(nanoseconds: 50_000_000)
|
||||
}
|
||||
XCTFail("timed out waiting for failed state")
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class MockHistoryImportCameraDriver: NSObject, CameraDriver {
|
||||
var platform: CameraPlatform = .canon
|
||||
var deviceInfo = CameraDeviceInfo(
|
||||
name: "Mock Camera",
|
||||
manufacturer: nil,
|
||||
model: nil,
|
||||
serialNumber: nil,
|
||||
usbVendorID: nil,
|
||||
usbProductID: nil
|
||||
)
|
||||
var objects: [CameraObject] = []
|
||||
|
||||
func connect() async throws {}
|
||||
func disconnect() {}
|
||||
|
||||
func listObjects() async throws -> [CameraObject] {
|
||||
objects
|
||||
}
|
||||
|
||||
func requestThumbnailData(for object: CameraObject, maxPixelSize: Int) async -> Data? {
|
||||
nil
|
||||
}
|
||||
|
||||
func downloadObject(_ object: CameraObject, to directory: URL) async throws -> URL {
|
||||
let url = directory.appendingPathComponent(object.filename)
|
||||
try Data(repeating: 0x01, count: 16).write(to: url)
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class MockHistoryImportPhotoSink: OTGPhotoSink {
|
||||
var existingIDs: Set<String> = []
|
||||
private(set) var savedCount = 0
|
||||
|
||||
func saveLiveShot(
|
||||
data: Data,
|
||||
filename: String,
|
||||
capturedAt: Date,
|
||||
cameraObjectID: String
|
||||
) async throws -> SavedOTGPhoto {
|
||||
savedCount += 1
|
||||
return SavedOTGPhoto(
|
||||
assetID: cameraObjectID,
|
||||
relativeLocalPath: "mock/\(filename)",
|
||||
filename: filename,
|
||||
capturedAt: capturedAt
|
||||
)
|
||||
}
|
||||
|
||||
func saveDownloadedFile(
|
||||
at url: URL,
|
||||
filename: String,
|
||||
capturedAt: Date,
|
||||
cameraObjectID: String
|
||||
) async throws -> SavedOTGPhoto {
|
||||
savedCount += 1
|
||||
return SavedOTGPhoto(
|
||||
assetID: cameraObjectID,
|
||||
relativeLocalPath: "mock/\(filename)",
|
||||
filename: filename,
|
||||
capturedAt: capturedAt
|
||||
)
|
||||
}
|
||||
|
||||
func existingCameraObjectIDs() async -> Set<String> {
|
||||
existingIDs
|
||||
}
|
||||
}
|
||||
@ -233,6 +233,47 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(store.loadTransferModeOption(), WiredCameraTransferViewModel.modePostShootTransfer)
|
||||
}
|
||||
|
||||
/// 测试 ConnectionState 映射为 CameraConnectionState。
|
||||
func testConnectionStateMapsToCameraConnectionState() {
|
||||
XCTAssertEqual(ConnectionState.idle.cameraConnectionState, .disconnected)
|
||||
XCTAssertEqual(ConnectionState.disconnected.cameraConnectionState, .disconnected)
|
||||
XCTAssertEqual(ConnectionState.searching.cameraConnectionState, .searching)
|
||||
XCTAssertEqual(ConnectionState.connecting(deviceName: "EOS R5").cameraConnectionState, .connecting)
|
||||
XCTAssertEqual(
|
||||
ConnectionState.connected(platform: .sony, deviceName: "ILCE-7M4").cameraConnectionState,
|
||||
.connected(deviceName: "ILCE-7M4")
|
||||
)
|
||||
XCTAssertEqual(
|
||||
ConnectionState.failed(message: "授权被拒绝").cameraConnectionState,
|
||||
.error("授权被拒绝")
|
||||
)
|
||||
}
|
||||
|
||||
/// 测试边拍边传 ingest 会触发管道自动上传。
|
||||
func testPipelineIngestAutoUploadUploadsAsset() async throws {
|
||||
let scope = CameraDownloadStorage.Scope(accountKey: "otg-pipe", albumID: 42)
|
||||
let fileURL = CameraDownloadStorage.uniqueLocalURL(for: "otg_live.JPG", scope: scope)
|
||||
try Data(repeating: 0xAA, count: 32).write(to: fileURL)
|
||||
defer { CameraDownloadStorage.removeScopeDirectory(for: scope) }
|
||||
|
||||
let pipeline = WiredCameraTransferPipeline()
|
||||
let sink = MockCameraAssetUploadSink()
|
||||
let relativePath = CameraDownloadStorage.relativePath(for: fileURL)
|
||||
|
||||
pipeline.configure(uploadSink: sink, uploadEnabled: true, downloadScope: scope)
|
||||
pipeline.ingestDownloadedAsset(
|
||||
assetID: "obj-1",
|
||||
filename: "otg_live.JPG",
|
||||
localPath: relativePath,
|
||||
capturedAt: Date(),
|
||||
autoUpload: true
|
||||
)
|
||||
|
||||
try await Task.sleep(nanoseconds: 800_000_000)
|
||||
XCTAssertEqual(sink.uploadedFileNames, ["otg_live.JPG"])
|
||||
XCTAssertEqual(pipeline.task(forAssetID: "obj-1")?.status, .uploaded)
|
||||
}
|
||||
|
||||
/// 测试进入页面时会恢复缓存的传输模式。
|
||||
func testStartRestoresCachedTransferModeOption() async {
|
||||
let account = makeTransferModeTestAccount()
|
||||
@ -466,6 +507,22 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class MockCameraAssetUploadSink: CameraAssetUploadSink {
|
||||
private(set) var uploadedFileNames: [String] = []
|
||||
|
||||
func upload(
|
||||
localURL: URL,
|
||||
fileName: String,
|
||||
fileType: Int,
|
||||
progress: @escaping @Sendable (Int) -> Void
|
||||
) async throws -> String {
|
||||
uploadedFileNames.append(fileName)
|
||||
progress(100)
|
||||
return "https://cdn/mock/\(fileName)"
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class MockTravelAlbumAPIForWiredTransfer: TravelAlbumServing {
|
||||
func availableOrders() async throws -> [TravelAlbumAvailableOrder] { [] }
|
||||
|
||||
Reference in New Issue
Block a user