Add TravelAlbum, ProfileSpace, and wired camera transfer modules.
Introduce travel album entry with Sony PTP tethering pipeline, profile space settings page, home routing updates, Launch Screen storyboard, and related tests/docs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -0,0 +1,70 @@
|
||||
//
|
||||
// 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)
|
||||
|
||||
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)
|
||||
try? FileManager.default.removeItem(at: tempURL)
|
||||
}
|
||||
}
|
||||
|
||||
@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 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)"
|
||||
}
|
||||
}
|
||||
63
suixinkanTests/CameraTransfer/SonyPTPCommandsTests.swift
Normal file
63
suixinkanTests/CameraTransfer/SonyPTPCommandsTests.swift
Normal file
@ -0,0 +1,63 @@
|
||||
//
|
||||
// SonyPTPCommandsTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
// Created by Codex on 2026/6/29.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// Sony PTP 命令构建与解析测试。
|
||||
final class SonyPTPCommandsTests: XCTestCase {
|
||||
/// 测试 OpenSession 命令包长度与 opcode。
|
||||
func testBuildOpenSessionCommand() {
|
||||
let data = SonyPTPHelper.buildCommand(
|
||||
code: SonyPTPCommand.openSession,
|
||||
transactionID: 1,
|
||||
parameters: [1]
|
||||
)
|
||||
|
||||
XCTAssertEqual(data.count, 16)
|
||||
XCTAssertEqual(data[6], 0x02)
|
||||
XCTAssertEqual(data[7], 0x10)
|
||||
}
|
||||
|
||||
/// 测试 SDIO Connect 命令 opcode。
|
||||
func testBuildSonyConnectCommand() {
|
||||
let data = SonyPTPHelper.buildCommand(
|
||||
code: SonyPTPCommand.sdioConnect,
|
||||
transactionID: 42,
|
||||
parameters: [1, 0, 0]
|
||||
)
|
||||
|
||||
XCTAssertEqual(data.count, 24)
|
||||
XCTAssertEqual(data[6], 0x01)
|
||||
XCTAssertEqual(data[7], 0x92)
|
||||
}
|
||||
|
||||
/// 测试 GetObject 压缩大小解析。
|
||||
func testParseObjectCompressedSize() {
|
||||
var data = Data(repeating: 0, count: 52)
|
||||
data[8] = 0x00
|
||||
data[9] = 0x10
|
||||
data[10] = 0x00
|
||||
data[11] = 0x00
|
||||
|
||||
XCTAssertEqual(SonyPTPHelper.parseObjectCompressedSize(data), 0x1000)
|
||||
}
|
||||
|
||||
/// 测试 SDIE ObjectAdded 事件解析。
|
||||
func testParseObjectAddedEvent() throws {
|
||||
var event = Data()
|
||||
event.append(contentsOf: [0x10, 0x00, 0x00, 0x00])
|
||||
event.append(contentsOf: [0x04, 0x00])
|
||||
event.append(contentsOf: [0x01, 0xC2])
|
||||
event.append(contentsOf: [0xFF, 0xFF, 0xFF, 0xFF])
|
||||
event.append(contentsOf: [0x01, 0xC0, 0xFF, 0xFF])
|
||||
|
||||
let container = try SonyPTPHelper.parseContainer(event)
|
||||
XCTAssertEqual(container.code, SonyPTPCommand.sdieObjectAdded)
|
||||
XCTAssertEqual(container.params.first, SonyPTPCommand.shotObjectHandle)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user