Files
suixinkan_ios_new/suixinkanTests/CameraTransfer/CameraTransferPipelineTests.swift
汉秋 5e620623eb 对齐旅拍相册详情页 Android UI,并修复网格预览与布局问题。
重构相册详情为信息卡片、Tab 筛选、排序、批量删除与底部上传栏;修复网格重叠、禁用按钮蒙层,并支持点击预览大图。同步扩展素材列表 API 与 ViewModel 分页逻辑,并优化有线传图缩略图与传输性能。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 13:53:59 +08:00

209 lines
7.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// CameraTransferPipelineTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/29.
//
import XCTest
@testable import suixinkan
@MainActor
/// 使 Mock Sink
final class CameraTransferPipelineTests: XCTestCase {
/// Sink
func testNewAssetTriggersUploadSink() async {
let camera = MockCameraService()
let sink = MockUploadSink()
let pipeline = CameraTransferPipeline(cameraService: camera)
pipeline.configure(uploadSink: sink, uploadEnabled: true)
var latestTasks: [CameraTransferTask] = []
pipeline.onTasksUpdated = { latestTasks = $0 }
let asset = CameraAsset(id: "ptp_1", filename: "DSC_001.JPG", fileSize: 1024)
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("DSC_001.JPG")
try? Data(repeating: 0xFF, count: 8).write(to: tempURL)
camera.onNewAsset?(asset)
try? await Task.sleep(nanoseconds: 300_000_000)
XCTAssertEqual(sink.uploadCount, 1)
let savedTask = latestTasks.first { $0.assetID == asset.id }
XCTAssertEqual(savedTask?.localPath?.hasPrefix("CameraDownloads/"), true)
XCTAssertEqual(savedTask?.localPath?.hasPrefix("/"), false)
if let localURL = savedTask?.localURL {
try? FileManager.default.removeItem(at: localURL)
}
try? FileManager.default.removeItem(at: tempURL)
}
///
func testRetryUploadRehydratesFromRelativePath() async throws {
let camera = MockCameraService()
let sink = MockUploadSink()
let pipeline = CameraTransferPipeline(cameraService: camera)
pipeline.configure(uploadSink: sink, uploadEnabled: false)
let fileURL = CameraDownloadStorage.uniqueLocalURL(for: "retry_pipeline.JPG")
try Data(repeating: 0xAB, count: 16).write(to: fileURL)
defer { try? FileManager.default.removeItem(at: fileURL) }
let relativePath = CameraDownloadStorage.relativePath(for: fileURL)
await pipeline.retryUpload(
assetID: "asset_retry_001",
filename: "retry_pipeline.JPG",
localPath: relativePath
)
XCTAssertEqual(sink.uploadCount, 1)
XCTAssertEqual(pipeline.task(forAssetID: "asset_retry_001")?.status, .uploaded)
}
///
func testBurstCaptureUploadsAllAssetsWhileFirstUploadInProgress() async throws {
let camera = MockBurstCameraService()
let sink = MockSlowUploadSink()
let pipeline = CameraTransferPipeline(cameraService: camera)
pipeline.configure(uploadSink: sink, uploadEnabled: true)
for index in 1 ... 10 {
let filename = "DSC_\(index).JPG"
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(filename)
try Data(repeating: UInt8(index), count: 8).write(to: tempURL)
camera.downloadURLs[filename] = tempURL
camera.onNewAsset?(
CameraAsset(id: "ptp_\(index)", filename: filename, fileSize: 1024)
)
}
try await Task.sleep(nanoseconds: 2_000_000_000)
XCTAssertEqual(sink.uploadCount, 10)
XCTAssertGreaterThan(sink.maxActiveUploads, 1)
XCTAssertLessThanOrEqual(sink.maxActiveUploads, 3)
for index in 1 ... 10 {
XCTAssertEqual(pipeline.task(forAssetID: "ptp_\(index)")?.status, .uploaded)
}
}
/// SwiftUI
func testUploadProgressNotificationsAreThrottled() async throws {
let camera = MockCameraService()
let sink = MockChattyUploadSink()
let pipeline = CameraTransferPipeline(cameraService: camera)
pipeline.configure(uploadSink: sink, uploadEnabled: true)
var updateCount = 0
pipeline.onTasksUpdated = { _ in updateCount += 1 }
camera.onNewAsset?(CameraAsset(id: "ptp_chatty", filename: "chatty.JPG", fileSize: 1024))
try await Task.sleep(nanoseconds: 800_000_000)
XCTAssertEqual(pipeline.task(forAssetID: "ptp_chatty")?.status, .uploaded)
XCTAssertLessThan(updateCount, 18)
}
}
@MainActor
private final class MockCameraService: CameraServiceProtocol {
let brand: CameraBrand = .sony
var connectionState: CameraConnectionState = .connected(deviceName: "Mock")
var onConnectionStateChange: ((CameraConnectionState) -> Void)?
var onNewAsset: ((CameraAsset) -> Void)?
func connect() async {}
func disconnect() async {}
func listAssets() async throws -> [CameraAsset] {
[]
}
func downloadAsset(_ asset: CameraAsset) async throws -> URL {
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 MockBurstCameraService: CameraServiceProtocol {
let brand: CameraBrand = .sony
var connectionState: CameraConnectionState = .connected(deviceName: "Mock")
var onConnectionStateChange: ((CameraConnectionState) -> Void)?
var onNewAsset: ((CameraAsset) -> Void)?
var downloadURLs: [String: URL] = [:]
func connect() async {}
func disconnect() async {}
func listAssets() async throws -> [CameraAsset] { [] }
func downloadAsset(_ asset: CameraAsset) async throws -> URL {
try await Task.sleep(nanoseconds: 30_000_000)
if let url = downloadURLs[asset.filename] {
return url
}
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 MockSlowUploadSink: CameraAssetUploadSink {
var uploadCount = 0
var activeUploads = 0
var maxActiveUploads = 0
func upload(
localURL: URL,
fileName: String,
fileType: Int,
progress: @escaping @Sendable (Int) -> Void
) async throws -> String {
uploadCount += 1
activeUploads += 1
maxActiveUploads = max(maxActiveUploads, activeUploads)
try await Task.sleep(nanoseconds: 150_000_000)
activeUploads -= 1
progress(100)
return "https://cdn/mock/\(fileName)"
}
}
@MainActor
private final class MockUploadSink: CameraAssetUploadSink {
var uploadCount = 0
func upload(
localURL: URL,
fileName: String,
fileType: Int,
progress: @escaping @Sendable (Int) -> Void
) async throws -> String {
uploadCount += 1
progress(100)
return "https://cdn/mock/\(fileName)"
}
}
@MainActor
private final class MockChattyUploadSink: CameraAssetUploadSink {
func upload(
localURL: URL,
fileName: String,
fileType: Int,
progress: @escaping @Sendable (Int) -> Void
) async throws -> String {
for value in 0 ... 100 {
progress(value)
}
return "https://cdn/mock/\(fileName)"
}
}