主要改动: - 移除调试导出功能(exportToDocuments 及相关 UI) - EditorView 添加封面帧预览和关键帧时间选择 - 新增 Analytics.swift 基础埋点模块(使用 os.Logger) - 创建 Live Photo 风格应用图标(SVG → PNG) - 优化 LivePhotoCore:简化代码结构,修复宽高比问题 - 添加单元测试资源文件 metadata.mov - 更新 TASK.md 进度追踪 M1 MVP 闭环已完成: ✅ 5个核心页面(Home/Editor/Processing/Result/WallpaperGuide) ✅ 时长裁剪 + 封面帧选择 ✅ 完整生成管线 + 相册保存 + 系统验证 ✅ 壁纸设置引导(iOS 16/17+ 差异化文案) ✅ 基础埋点事件追踪 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
286 lines
9.8 KiB
Swift
286 lines
9.8 KiB
Swift
import XCTest
|
|
@testable import LivePhotoCore
|
|
|
|
final class LivePhotoCoreTests: XCTestCase {
|
|
|
|
// MARK: - ExportParams Tests
|
|
|
|
func testExportParamsDefaults() {
|
|
let params = ExportParams()
|
|
|
|
XCTAssertEqual(params.trimStart, 0)
|
|
XCTAssertEqual(params.trimEnd, 1.0)
|
|
XCTAssertEqual(params.keyFrameTime, 0.5)
|
|
XCTAssertEqual(params.audioPolicy, .keep)
|
|
XCTAssertEqual(params.codecPolicy, .fallbackH264)
|
|
XCTAssertEqual(params.hdrPolicy, .toneMapToSDR)
|
|
XCTAssertEqual(params.maxDimension, 1920)
|
|
}
|
|
|
|
func testExportParamsCustomValues() {
|
|
let params = ExportParams(
|
|
trimStart: 0.5,
|
|
trimEnd: 2.0,
|
|
keyFrameTime: 1.0,
|
|
audioPolicy: .remove,
|
|
codecPolicy: .passthrough,
|
|
hdrPolicy: .keep,
|
|
maxDimension: 1080
|
|
)
|
|
|
|
XCTAssertEqual(params.trimStart, 0.5)
|
|
XCTAssertEqual(params.trimEnd, 2.0)
|
|
XCTAssertEqual(params.keyFrameTime, 1.0)
|
|
XCTAssertEqual(params.audioPolicy, .remove)
|
|
XCTAssertEqual(params.codecPolicy, .passthrough)
|
|
XCTAssertEqual(params.hdrPolicy, .keep)
|
|
XCTAssertEqual(params.maxDimension, 1080)
|
|
}
|
|
|
|
func testExportParamsCodable() throws {
|
|
let original = ExportParams(
|
|
trimStart: 1.0,
|
|
trimEnd: 3.0,
|
|
keyFrameTime: 2.0,
|
|
audioPolicy: .remove,
|
|
codecPolicy: .passthrough,
|
|
hdrPolicy: .keep,
|
|
maxDimension: 720
|
|
)
|
|
|
|
let encoded = try JSONEncoder().encode(original)
|
|
let decoded = try JSONDecoder().decode(ExportParams.self, from: encoded)
|
|
|
|
XCTAssertEqual(decoded.trimStart, original.trimStart)
|
|
XCTAssertEqual(decoded.trimEnd, original.trimEnd)
|
|
XCTAssertEqual(decoded.keyFrameTime, original.keyFrameTime)
|
|
XCTAssertEqual(decoded.audioPolicy, original.audioPolicy)
|
|
XCTAssertEqual(decoded.codecPolicy, original.codecPolicy)
|
|
XCTAssertEqual(decoded.hdrPolicy, original.hdrPolicy)
|
|
XCTAssertEqual(decoded.maxDimension, original.maxDimension)
|
|
}
|
|
|
|
// MARK: - AppError Tests
|
|
|
|
func testAppErrorInit() {
|
|
let error = AppError(
|
|
code: "LPB-101",
|
|
stage: .normalize,
|
|
message: "Test error",
|
|
underlyingErrorDescription: "Underlying",
|
|
suggestedActions: ["Action 1", "Action 2"]
|
|
)
|
|
|
|
XCTAssertEqual(error.code, "LPB-101")
|
|
XCTAssertEqual(error.stage, .normalize)
|
|
XCTAssertEqual(error.message, "Test error")
|
|
XCTAssertEqual(error.underlyingErrorDescription, "Underlying")
|
|
XCTAssertEqual(error.suggestedActions, ["Action 1", "Action 2"])
|
|
}
|
|
|
|
func testAppErrorCodable() throws {
|
|
let original = AppError(
|
|
code: "LPB-201",
|
|
stage: .extractKeyFrame,
|
|
message: "封面生成失败",
|
|
underlyingErrorDescription: nil,
|
|
suggestedActions: ["重试"]
|
|
)
|
|
|
|
let encoded = try JSONEncoder().encode(original)
|
|
let decoded = try JSONDecoder().decode(AppError.self, from: encoded)
|
|
|
|
XCTAssertEqual(decoded.code, original.code)
|
|
XCTAssertEqual(decoded.stage, original.stage)
|
|
XCTAssertEqual(decoded.message, original.message)
|
|
XCTAssertEqual(decoded.suggestedActions, original.suggestedActions)
|
|
}
|
|
|
|
// MARK: - SourceRef Tests
|
|
|
|
func testSourceRefWithAssetIdentifier() {
|
|
let ref = SourceRef(phAssetLocalIdentifier: "ABC123")
|
|
|
|
XCTAssertEqual(ref.phAssetLocalIdentifier, "ABC123")
|
|
XCTAssertNil(ref.fileURL)
|
|
}
|
|
|
|
func testSourceRefWithFileURL() {
|
|
let url = URL(fileURLWithPath: "/tmp/test.mov")
|
|
let ref = SourceRef(fileURL: url)
|
|
|
|
XCTAssertNil(ref.phAssetLocalIdentifier)
|
|
XCTAssertEqual(ref.fileURL, url)
|
|
}
|
|
|
|
// MARK: - WorkItem Tests
|
|
|
|
func testWorkItemDefaults() {
|
|
let cacheDir = URL(fileURLWithPath: "/tmp/cache")
|
|
let sourceRef = SourceRef(phAssetLocalIdentifier: "test-id")
|
|
|
|
let item = WorkItem(
|
|
sourceVideo: sourceRef,
|
|
cacheDir: cacheDir
|
|
)
|
|
|
|
XCTAssertNotNil(item.id)
|
|
XCTAssertNotNil(item.createdAt)
|
|
XCTAssertEqual(item.status, .idle)
|
|
XCTAssertNil(item.resultAssetId)
|
|
XCTAssertNil(item.error)
|
|
XCTAssertNil(item.coverImage)
|
|
}
|
|
|
|
// MARK: - LivePhotoBuildProgress Tests
|
|
|
|
func testLivePhotoBuildProgress() {
|
|
let progress = LivePhotoBuildProgress(stage: .normalize, fraction: 0.5)
|
|
|
|
XCTAssertEqual(progress.stage, .normalize)
|
|
XCTAssertEqual(progress.fraction, 0.5)
|
|
}
|
|
|
|
// MARK: - LivePhotoBuildStage Tests
|
|
|
|
func testLivePhotoBuildStageRawValues() {
|
|
XCTAssertEqual(LivePhotoBuildStage.normalize.rawValue, "normalize")
|
|
XCTAssertEqual(LivePhotoBuildStage.extractKeyFrame.rawValue, "extractKeyFrame")
|
|
XCTAssertEqual(LivePhotoBuildStage.writePhotoMetadata.rawValue, "writePhotoMetadata")
|
|
XCTAssertEqual(LivePhotoBuildStage.writeVideoMetadata.rawValue, "writeVideoMetadata")
|
|
XCTAssertEqual(LivePhotoBuildStage.saveToAlbum.rawValue, "saveToAlbum")
|
|
XCTAssertEqual(LivePhotoBuildStage.validate.rawValue, "validate")
|
|
}
|
|
|
|
// MARK: - WorkStatus Tests
|
|
|
|
func testWorkStatusRawValues() {
|
|
XCTAssertEqual(WorkStatus.idle.rawValue, "idle")
|
|
XCTAssertEqual(WorkStatus.editing.rawValue, "editing")
|
|
XCTAssertEqual(WorkStatus.processing.rawValue, "processing")
|
|
XCTAssertEqual(WorkStatus.success.rawValue, "success")
|
|
XCTAssertEqual(WorkStatus.failed.rawValue, "failed")
|
|
}
|
|
|
|
// MARK: - CacheManager Tests
|
|
|
|
func testCacheManagerInit() throws {
|
|
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
let manager = try CacheManager(baseDirectory: tempDir)
|
|
|
|
XCTAssertEqual(manager.baseDirectory, tempDir)
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: tempDir.path))
|
|
|
|
// Cleanup
|
|
try? FileManager.default.removeItem(at: tempDir)
|
|
}
|
|
|
|
func testCacheManagerMakeWorkPaths() throws {
|
|
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
let manager = try CacheManager(baseDirectory: tempDir)
|
|
let workId = UUID()
|
|
|
|
let paths = try manager.makeWorkPaths(workId: workId)
|
|
|
|
XCTAssertTrue(paths.workDir.path.contains(workId.uuidString))
|
|
XCTAssertEqual(paths.photoURL.pathExtension, "heic")
|
|
XCTAssertEqual(paths.pairedVideoURL.pathExtension, "mov")
|
|
XCTAssertEqual(paths.logURL.pathExtension, "log")
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: paths.workDir.path))
|
|
|
|
// Cleanup
|
|
try? FileManager.default.removeItem(at: tempDir)
|
|
}
|
|
|
|
func testCacheManagerClearWork() throws {
|
|
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
let manager = try CacheManager(baseDirectory: tempDir)
|
|
let workId = UUID()
|
|
|
|
// Create work directory
|
|
let paths = try manager.makeWorkPaths(workId: workId)
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: paths.workDir.path))
|
|
|
|
// Clear it
|
|
try manager.clearWork(workId: workId)
|
|
XCTAssertFalse(FileManager.default.fileExists(atPath: paths.workDir.path))
|
|
|
|
// Cleanup
|
|
try? FileManager.default.removeItem(at: tempDir)
|
|
}
|
|
|
|
// MARK: - LivePhotoWorkPaths Tests
|
|
|
|
func testLivePhotoWorkPaths() {
|
|
let workDir = URL(fileURLWithPath: "/tmp/work")
|
|
let photoURL = URL(fileURLWithPath: "/tmp/work/photo.heic")
|
|
let pairedVideoURL = URL(fileURLWithPath: "/tmp/work/paired.mov")
|
|
let logURL = URL(fileURLWithPath: "/tmp/work/builder.log")
|
|
|
|
let paths = LivePhotoWorkPaths(
|
|
workDir: workDir,
|
|
photoURL: photoURL,
|
|
pairedVideoURL: pairedVideoURL,
|
|
logURL: logURL
|
|
)
|
|
|
|
XCTAssertEqual(paths.workDir, workDir)
|
|
XCTAssertEqual(paths.photoURL, photoURL)
|
|
XCTAssertEqual(paths.pairedVideoURL, pairedVideoURL)
|
|
XCTAssertEqual(paths.logURL, logURL)
|
|
}
|
|
|
|
// MARK: - LivePhotoBuildOutput Tests
|
|
|
|
func testLivePhotoBuildOutput() {
|
|
let workId = UUID()
|
|
let assetId = "test-asset-id"
|
|
let photoURL = URL(fileURLWithPath: "/tmp/photo.heic")
|
|
let videoURL = URL(fileURLWithPath: "/tmp/paired.mov")
|
|
|
|
let output = LivePhotoBuildOutput(
|
|
workId: workId,
|
|
assetIdentifier: assetId,
|
|
pairedImageURL: photoURL,
|
|
pairedVideoURL: videoURL
|
|
)
|
|
|
|
XCTAssertEqual(output.workId, workId)
|
|
XCTAssertEqual(output.assetIdentifier, assetId)
|
|
XCTAssertEqual(output.pairedImageURL, photoURL)
|
|
XCTAssertEqual(output.pairedVideoURL, videoURL)
|
|
}
|
|
|
|
// MARK: - Policy Enums Tests
|
|
|
|
func testAudioPolicyCodable() throws {
|
|
let policies: [AudioPolicy] = [.keep, .remove]
|
|
|
|
for policy in policies {
|
|
let encoded = try JSONEncoder().encode(policy)
|
|
let decoded = try JSONDecoder().decode(AudioPolicy.self, from: encoded)
|
|
XCTAssertEqual(decoded, policy)
|
|
}
|
|
}
|
|
|
|
func testCodecPolicyCodable() throws {
|
|
let policies: [CodecPolicy] = [.passthrough, .fallbackH264]
|
|
|
|
for policy in policies {
|
|
let encoded = try JSONEncoder().encode(policy)
|
|
let decoded = try JSONDecoder().decode(CodecPolicy.self, from: encoded)
|
|
XCTAssertEqual(decoded, policy)
|
|
}
|
|
}
|
|
|
|
func testHDRPolicyCodable() throws {
|
|
let policies: [HDRPolicy] = [.keep, .toneMapToSDR]
|
|
|
|
for policy in policies {
|
|
let encoded = try JSONEncoder().encode(policy)
|
|
let decoded = try JSONDecoder().decode(HDRPolicy.self, from: encoded)
|
|
XCTAssertEqual(decoded, policy)
|
|
}
|
|
}
|
|
}
|