353 lines
13 KiB
Swift
353 lines
13 KiB
Swift
//
|
|
// TravelAlbumModelsTests.swift
|
|
// suixinkanTests
|
|
//
|
|
|
|
import XCTest
|
|
@testable import suixinkan
|
|
|
|
/// 旅拍相册模型与展示工具测试。
|
|
final class TravelAlbumModelsTests: XCTestCase {
|
|
func testPreviewProjectMapsMaterialToOriginalAsset() {
|
|
let material = TravelAlbumMaterial(
|
|
id: 8,
|
|
fileName: "IMG_0008.JPG",
|
|
fileUrl: "https://cdn.example.com/original.jpg",
|
|
fileSize: 4096,
|
|
coverUrl: "https://cdn.example.com/cover.jpg"
|
|
)
|
|
|
|
let project = TravelAlbumPreviewProject(material: material)
|
|
|
|
XCTAssertEqual(project.originalMaterialId, 8)
|
|
XCTAssertEqual(project.orderedAssets.map(\.kind), [.original])
|
|
XCTAssertEqual(project.orderedAssets.first?.displayURL, material.fileUrl)
|
|
XCTAssertEqual(project.orderedAssets.first?.previewURL, material.coverUrl)
|
|
XCTAssertFalse(project.hasVariants)
|
|
}
|
|
|
|
func testPreviewProjectMapsNonemptyAIResultURLsInCanonicalOrder() {
|
|
let material = TravelAlbumMaterial(
|
|
id: 8,
|
|
fileName: "IMG_0008.JPG",
|
|
fileUrl: "https://cdn.example.com/original.jpg",
|
|
fileSize: 4096,
|
|
coverUrl: "https://cdn.example.com/cover.jpg",
|
|
aiRefinedURL: " https://cdn.example.com/refined.jpg ",
|
|
aiAtmosphereURL: "https://cdn.example.com/atmosphere.jpg"
|
|
)
|
|
|
|
let project = TravelAlbumPreviewProject(material: material)
|
|
|
|
XCTAssertEqual(project.orderedAssets.map(\.kind), [.original, .retouched, .atmosphere])
|
|
XCTAssertEqual(
|
|
project.asset(for: .retouched)?.displayURL,
|
|
"https://cdn.example.com/refined.jpg"
|
|
)
|
|
XCTAssertEqual(
|
|
project.asset(for: .atmosphere)?.previewURL,
|
|
"https://cdn.example.com/atmosphere.jpg"
|
|
)
|
|
XCTAssertNil(project.asset(for: .cover))
|
|
XCTAssertTrue(project.hasVariants)
|
|
}
|
|
|
|
func testPreviewProjectIgnoresBlankAIResultURLs() {
|
|
let material = TravelAlbumMaterial(
|
|
id: 8,
|
|
fileUrl: "https://cdn.example.com/original.jpg",
|
|
aiRefinedURL: " \n ",
|
|
aiAtmosphereURL: "\t"
|
|
)
|
|
|
|
XCTAssertEqual(
|
|
TravelAlbumPreviewProject(material: material).orderedAssets.map(\.kind),
|
|
[.original]
|
|
)
|
|
}
|
|
|
|
func testPreviewAssetFallsBackToAvailableURLForBothQualityLevels() {
|
|
let missingOriginal = TravelAlbumPreviewAsset(
|
|
id: "cover-only",
|
|
kind: .cover,
|
|
fileURL: " ",
|
|
coverURL: "https://cdn.example.com/cover.jpg",
|
|
fileName: "cover.jpg",
|
|
fileSize: 100
|
|
)
|
|
let missingCover = TravelAlbumPreviewAsset(
|
|
id: "original-only",
|
|
kind: .original,
|
|
fileURL: "https://cdn.example.com/original.jpg",
|
|
coverURL: "\n",
|
|
fileName: "original.jpg",
|
|
fileSize: 200
|
|
)
|
|
|
|
XCTAssertEqual(missingOriginal.displayURL, missingOriginal.coverURL)
|
|
XCTAssertEqual(missingOriginal.previewURL, missingOriginal.coverURL)
|
|
XCTAssertEqual(missingCover.displayURL, missingCover.fileURL)
|
|
XCTAssertEqual(missingCover.previewURL, missingCover.fileURL)
|
|
}
|
|
|
|
func testPreviewProjectKeepsCanonicalExistingVariantOrder() {
|
|
let project = makePreviewProject(kinds: [.cover, .original, .atmosphere])
|
|
|
|
XCTAssertEqual(project.orderedAssets.map(\.kind), [.original, .atmosphere, .cover])
|
|
XCTAssertTrue(project.hasVariants)
|
|
}
|
|
|
|
func testPreviewNavigatorBuildsNodesForBothModes() {
|
|
let projects = [
|
|
makePreviewProject(id: 1, kinds: [.original, .retouched]),
|
|
makePreviewProject(id: 2, kinds: [.original, .cover]),
|
|
]
|
|
|
|
XCTAssertEqual(
|
|
TravelAlbumPreviewNavigator.nodes(projects: projects, mode: .projectsOnly),
|
|
[
|
|
TravelAlbumPreviewNode(projectIndex: 0, kind: .original),
|
|
TravelAlbumPreviewNode(projectIndex: 1, kind: .original),
|
|
]
|
|
)
|
|
XCTAssertEqual(
|
|
TravelAlbumPreviewNavigator.nodes(projects: projects, mode: .includeVariants).map(\.kind),
|
|
[.original, .retouched, .original, .cover]
|
|
)
|
|
}
|
|
|
|
func testPreviewNavigatorBackwardFromOriginalTargetsPreviousOriginal() {
|
|
let projects = [
|
|
makePreviewProject(id: 1, kinds: [.original, .retouched, .cover]),
|
|
makePreviewProject(id: 2, kinds: [.original, .atmosphere]),
|
|
]
|
|
let nodes = TravelAlbumPreviewNavigator.nodes(projects: projects, mode: .includeVariants)
|
|
let secondOriginal = nodes.firstIndex {
|
|
$0.projectIndex == 1 && $0.kind == .original
|
|
}!
|
|
|
|
let target = TravelAlbumPreviewNavigator.backwardTargetIndex(nodes: nodes, currentIndex: secondOriginal)
|
|
|
|
XCTAssertEqual(nodes[target], TravelAlbumPreviewNode(projectIndex: 0, kind: .original))
|
|
}
|
|
|
|
func testPreviewDeletionPrefersNextThenFallsBackToPreviousAndClosesWhenEmpty() {
|
|
XCTAssertEqual(
|
|
TravelAlbumPreviewNavigator.projectIndexAfterDeletion(
|
|
deletedProjectIndex: 0,
|
|
remainingProjectCount: 2
|
|
),
|
|
0
|
|
)
|
|
XCTAssertEqual(
|
|
TravelAlbumPreviewNavigator.projectIndexAfterDeletion(
|
|
deletedProjectIndex: 1,
|
|
remainingProjectCount: 2
|
|
),
|
|
1
|
|
)
|
|
XCTAssertEqual(
|
|
TravelAlbumPreviewNavigator.projectIndexAfterDeletion(
|
|
deletedProjectIndex: 2,
|
|
remainingProjectCount: 2
|
|
),
|
|
1
|
|
)
|
|
XCTAssertNil(
|
|
TravelAlbumPreviewNavigator.projectIndexAfterDeletion(
|
|
deletedProjectIndex: 0,
|
|
remainingProjectCount: 0
|
|
)
|
|
)
|
|
}
|
|
|
|
func testPlaceholderPreviewActionsReturnUnavailableWithoutMutation() async {
|
|
let handler = PlaceholderTravelAlbumPreviewActionHandler()
|
|
let aiResult = await handler.requestAIRetouch(originalMaterialId: 9)
|
|
let deleteResult = await handler.deleteProject(originalMaterialId: 9)
|
|
let refreshResult = await handler.refreshProject(originalMaterialId: 9)
|
|
|
|
XCTAssertEqual(
|
|
aiResult,
|
|
.unavailable("AI修图功能待接口接入")
|
|
)
|
|
XCTAssertEqual(
|
|
deleteResult,
|
|
.unavailable("项目删除接口待接入")
|
|
)
|
|
XCTAssertEqual(
|
|
refreshResult,
|
|
.unavailable("关联图片刷新接口待接入")
|
|
)
|
|
}
|
|
|
|
func testTravelAlbumDecodesSnakeCaseFields() throws {
|
|
let json = """
|
|
{
|
|
"id": 7,
|
|
"store_user_id": 2,
|
|
"name": "旅拍相册",
|
|
"type": 1,
|
|
"order_number": "NO001",
|
|
"material_num": 3,
|
|
"material_price": 1200,
|
|
"material_package_price": 5000,
|
|
"photo_price": 0,
|
|
"cover_url": "https://cdn/cover.jpg",
|
|
"user_id": 9,
|
|
"status": 1,
|
|
"created_at": "2026-07-07 10:00:00",
|
|
"updated_at": "2026-07-07 10:10:00",
|
|
"user": {"id": 4, "phone": "13800138000"}
|
|
}
|
|
""".data(using: .utf8)!
|
|
|
|
let album = try JSONDecoder().decode(TravelAlbum.self, from: json)
|
|
XCTAssertEqual(album.id, 7)
|
|
XCTAssertEqual(album.storeUserId, 2)
|
|
XCTAssertEqual(album.orderNumber, "NO001")
|
|
XCTAssertEqual(album.displayPhone, "13800138000")
|
|
}
|
|
|
|
func testTravelAlbumMaterialDecodesSnakeCaseFields() throws {
|
|
let json = """
|
|
{
|
|
"id": 11,
|
|
"user_equity_travel_id": 7,
|
|
"status": 1,
|
|
"order_number": "",
|
|
"user_id": 9,
|
|
"file_name": "IMG_0001.JPG",
|
|
"file_type": 2,
|
|
"file_url": "https://cdn/a.jpg",
|
|
"file_size": 2048,
|
|
"cover_url": "",
|
|
"is_purchased": false,
|
|
"ai_retouch_status": 3,
|
|
"ai_retouch_status_name": "AI已修",
|
|
"ai_refined_url": "https://cdn/refined.jpg",
|
|
"ai_atmosphere_url": "https://cdn/atmosphere.jpg",
|
|
"created_at": "",
|
|
"updated_at": ""
|
|
}
|
|
""".data(using: .utf8)!
|
|
|
|
let material = try JSONDecoder().decode(TravelAlbumMaterial.self, from: json)
|
|
XCTAssertEqual(material.userEquityTravelId, 7)
|
|
XCTAssertEqual(material.fileName, "IMG_0001.JPG")
|
|
XCTAssertFalse(material.isPurchased)
|
|
XCTAssertEqual(material.aiRetouchStatus, 3)
|
|
XCTAssertEqual(material.aiRetouchStatusName, "AI已修")
|
|
XCTAssertEqual(material.aiRefinedURL, "https://cdn/refined.jpg")
|
|
XCTAssertEqual(material.aiAtmosphereURL, "https://cdn/atmosphere.jpg")
|
|
}
|
|
|
|
func testTravelAlbumMaterialDefaultsInvalidAIFields() throws {
|
|
let json = """
|
|
{
|
|
"id": 11,
|
|
"user_equity_travel_id": 7,
|
|
"status": 1,
|
|
"order_number": "",
|
|
"user_id": 9,
|
|
"file_name": "IMG_0001.JPG",
|
|
"file_type": 2,
|
|
"file_url": "https://cdn/a.jpg",
|
|
"file_size": 2048,
|
|
"cover_url": "",
|
|
"is_purchased": false,
|
|
"ai_retouch_status": "invalid",
|
|
"ai_retouch_status_name": null,
|
|
"ai_refined_url": 123,
|
|
"created_at": "",
|
|
"updated_at": ""
|
|
}
|
|
""".data(using: .utf8)!
|
|
|
|
let material = try JSONDecoder().decode(TravelAlbumMaterial.self, from: json)
|
|
|
|
XCTAssertEqual(material.aiRetouchStatus, 0)
|
|
XCTAssertEqual(material.aiRetouchStatusName, "")
|
|
XCTAssertEqual(material.aiRefinedURL, "")
|
|
XCTAssertEqual(material.aiAtmosphereURL, "")
|
|
}
|
|
|
|
func testTravelAlbumMaterialBadgePriorityAndFallbacks() {
|
|
XCTAssertNil(TravelAlbumMaterial(aiRetouchStatus: 0).badgePresentation)
|
|
XCTAssertEqual(
|
|
TravelAlbumMaterial(isPurchased: true, aiRetouchStatus: 0, aiRetouchStatusName: "已上传")
|
|
.badgePresentation,
|
|
TravelAlbumMaterialBadgePresentation(kind: .purchased, text: "已购")
|
|
)
|
|
XCTAssertEqual(
|
|
TravelAlbumMaterial(aiRetouchStatus: 1, aiRetouchStatusName: " 排队中 ").badgePresentation,
|
|
TravelAlbumMaterialBadgePresentation(kind: .pending, text: "排队中")
|
|
)
|
|
XCTAssertEqual(
|
|
TravelAlbumMaterial(aiRetouchStatus: 2, aiRetouchStatusName: " ").badgePresentation,
|
|
TravelAlbumMaterialBadgePresentation(kind: .processing, text: "修图中")
|
|
)
|
|
XCTAssertEqual(
|
|
TravelAlbumMaterial(aiRetouchStatus: 3).badgePresentation,
|
|
TravelAlbumMaterialBadgePresentation(kind: .retouched, text: "AI已修")
|
|
)
|
|
XCTAssertEqual(
|
|
TravelAlbumMaterial(aiRetouchStatus: 3, aiRetouchStatusName: "AI封面").badgePresentation,
|
|
TravelAlbumMaterialBadgePresentation(kind: .cover, text: "AI封面")
|
|
)
|
|
XCTAssertEqual(
|
|
TravelAlbumMaterial(aiRetouchStatus: 4).badgePresentation,
|
|
TravelAlbumMaterialBadgePresentation(kind: .failed, text: "失败")
|
|
)
|
|
XCTAssertNil(TravelAlbumMaterial(aiRetouchStatus: 99, aiRetouchStatusName: "未知").badgePresentation)
|
|
}
|
|
|
|
func testDisplayFormatters() {
|
|
XCTAssertEqual(TravelAlbumDisplayFormatter.maskPhone("13800138000"), "138****8000")
|
|
XCTAssertEqual(TravelAlbumDisplayFormatter.maskPhone(""), "--")
|
|
XCTAssertEqual(TravelAlbumDisplayFormatter.fileSizeText(2048), "2.0KB")
|
|
XCTAssertEqual(TravelAlbumDisplayFormatter.fileSizeText(2 * 1024 * 1024), "2.0MB")
|
|
}
|
|
|
|
private func makePreviewProject(
|
|
id: Int = 1,
|
|
kinds: [TravelAlbumPreviewAssetKind]
|
|
) -> TravelAlbumPreviewProject {
|
|
TravelAlbumPreviewProject(
|
|
originalMaterialId: id,
|
|
assets: kinds.map { kind in
|
|
TravelAlbumPreviewAsset(
|
|
id: "\(id)-\(kind.rawValue)",
|
|
kind: kind,
|
|
fileURL: "https://cdn.example.com/\(id)-\(kind.rawValue).jpg",
|
|
coverURL: "",
|
|
fileName: "\(kind.title).jpg",
|
|
fileSize: 1024
|
|
)
|
|
}
|
|
)
|
|
}
|
|
|
|
@MainActor
|
|
func testBuildTodayAlbumNameUsesExistingSameDayCount() {
|
|
let date = Calendar(identifier: .gregorian).date(from: DateComponents(year: 2026, month: 7, day: 7))!
|
|
let viewModel = TravelAlbumEntryViewModel(currentScenicIdProvider: { 1 }, dateProvider: { date })
|
|
viewModel.onStateChange = {}
|
|
let api = TravelAlbumMockAPI()
|
|
api.listResponse = TravelAlbumListResponse(
|
|
total: 2,
|
|
list: [
|
|
TravelAlbum(id: 1, createdAt: "2026-07-07 09:00:00"),
|
|
TravelAlbum(id: 2, createdAt: "2026-07-06 09:00:00"),
|
|
]
|
|
)
|
|
let exp = expectation(description: "load")
|
|
Task {
|
|
await viewModel.loadAlbums(api: api)
|
|
XCTAssertEqual(viewModel.buildTodayAlbumName(), "2026-07-07-002")
|
|
exp.fulfill()
|
|
}
|
|
wait(for: [exp], timeout: 1)
|
|
}
|
|
}
|