Add album management and trailer upload to Assets module.
Wire home routing for album_list and album_trailer, add a DEBUG home menu preview in Profile, and expand Assets tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -144,6 +144,121 @@ final class AssetsViewModelTests: XCTestCase {
|
||||
XCTAssertFalse(success)
|
||||
XCTAssertEqual(api.mediaUploadRequests.count, 0)
|
||||
}
|
||||
|
||||
/// 测试相册列表无景区时清空数据且不请求接口。
|
||||
func testAlbumListClearsWithoutScenic() async {
|
||||
let api = MockAssetsService()
|
||||
let viewModel = AlbumListViewModel()
|
||||
|
||||
await viewModel.reload(api: api, scenicId: nil)
|
||||
|
||||
XCTAssertTrue(viewModel.folders.isEmpty)
|
||||
XCTAssertEqual(api.albumFolderListRequests.count, 0)
|
||||
XCTAssertEqual(viewModel.errorMessage, "缺少当前景区,无法加载相册")
|
||||
}
|
||||
|
||||
/// 测试相册列表搜索、日期筛选和分页参数。
|
||||
func testAlbumListFilterAndPagination() async {
|
||||
let api = MockAssetsService()
|
||||
api.albumFolderPages = [
|
||||
ListPayload(total: 2, list: [AlbumFolderItem.fixture(id: 1, name: "旅拍")]),
|
||||
ListPayload(total: 2, list: [AlbumFolderItem.fixture(id: 2, name: "亲子")])
|
||||
]
|
||||
let viewModel = AlbumListViewModel()
|
||||
viewModel.searchText = " 旅拍 "
|
||||
viewModel.startTime = "2026-06-01"
|
||||
viewModel.endTime = "2026-06-24"
|
||||
|
||||
await viewModel.reload(api: api, scenicId: 9)
|
||||
await viewModel.loadMore(api: api, scenicId: 9)
|
||||
|
||||
XCTAssertEqual(api.albumFolderListRequests.first?.name, "旅拍")
|
||||
XCTAssertEqual(api.albumFolderListRequests.first?.startTime, "2026-06-01")
|
||||
XCTAssertEqual(api.albumFolderListRequests.last?.page, 2)
|
||||
XCTAssertEqual(viewModel.folders.map(\.id), [1, 2])
|
||||
}
|
||||
|
||||
/// 测试新建相册成功后刷新列表。
|
||||
func testAlbumCreateRefreshesList() async {
|
||||
let api = MockAssetsService()
|
||||
api.albumFolderPages = [ListPayload(total: 1, list: [AlbumFolderItem.fixture(id: 3, name: "新相册")])]
|
||||
let viewModel = AlbumListViewModel()
|
||||
|
||||
let success = await viewModel.createFolder(name: " 新相册 ", remark: " 备注 ", scenicId: 9, api: api)
|
||||
|
||||
XCTAssertTrue(success)
|
||||
XCTAssertEqual(api.albumAddRequests.first?.name, "新相册")
|
||||
XCTAssertEqual(api.albumAddRequests.first?.remark, "备注")
|
||||
XCTAssertEqual(viewModel.folders.first?.id, 3)
|
||||
}
|
||||
|
||||
/// 测试相册详情加载信息和文件列表,切换 Tab 会重置分页。
|
||||
func testAlbumDetailLoadsInfoAndSwitchesTab() async {
|
||||
let api = MockAssetsService()
|
||||
api.albumFilePages = [
|
||||
ListPayload(total: 1, list: [AlbumFileItem.fixture(id: 11, fileType: 2)]),
|
||||
ListPayload(total: 1, list: [AlbumFileItem.fixture(id: 12, fileType: 1)])
|
||||
]
|
||||
let viewModel = AlbumDetailViewModel(folderId: 3)
|
||||
|
||||
await viewModel.reload(api: api, scenicId: 9)
|
||||
await viewModel.selectTab(.video, api: api, scenicId: 9)
|
||||
|
||||
XCTAssertEqual(api.albumInfoRequests, [3, 3])
|
||||
XCTAssertEqual(api.albumFileListRequests.map { $0.fileType }, [2, 1])
|
||||
XCTAssertEqual(viewModel.files.first?.id, 12)
|
||||
XCTAssertEqual(viewModel.page, 1)
|
||||
}
|
||||
|
||||
/// 测试相册详情删除、设置封面和编辑信息会调用正确接口。
|
||||
func testAlbumDetailMutationsUseExpectedRequests() async {
|
||||
let api = MockAssetsService()
|
||||
let viewModel = AlbumDetailViewModel(folderId: 3)
|
||||
let file = AlbumFileItem.fixture(id: 11, fileType: 2)
|
||||
|
||||
_ = await viewModel.delete(file: file, scenicId: 9, api: api)
|
||||
_ = await viewModel.setCover(file: file, scenicId: 9, api: api)
|
||||
_ = await viewModel.rename(name: " 改名 ", scenicId: 9, api: api)
|
||||
_ = await viewModel.updateRemark(" 新备注 ", scenicId: 9, api: api)
|
||||
|
||||
XCTAssertEqual(api.albumDeleteRequests.first?.idList, [11])
|
||||
XCTAssertEqual(api.albumEditRequests[0].coverFileId, 11)
|
||||
XCTAssertEqual(api.albumEditRequests[1].name, "改名")
|
||||
XCTAssertEqual(api.albumEditRequests[2].remark, "新备注")
|
||||
}
|
||||
|
||||
/// 测试相册预览上传会先上传 OSS,再登记相册文件。
|
||||
func testAlbumTrailerUploadUploadsOSSBeforeRegisteringFile() async {
|
||||
let api = MockAssetsService()
|
||||
api.albumFolderPages = [ListPayload(total: 1, list: [AlbumFolderItem.fixture(id: 3, name: "旅拍")])]
|
||||
let uploader = MockAssetsUploader()
|
||||
let viewModel = AlbumTrailerViewModel()
|
||||
viewModel.addLocalFiles([AlbumLocalUploadFile(data: Data([1]), fileName: "a.jpg", fileType: 2)])
|
||||
await viewModel.loadFolders(api: api, scenicId: 9)
|
||||
|
||||
let success = await viewModel.submit(scenicId: 9, api: api, uploadService: uploader)
|
||||
|
||||
XCTAssertTrue(success)
|
||||
XCTAssertEqual(uploader.uploadedFiles.map(\.fileName), ["a.jpg"])
|
||||
XCTAssertEqual(api.albumUploadURLRequests.first?.fileURL, "https://cdn.example.com/a.jpg")
|
||||
XCTAssertTrue(viewModel.localFiles.isEmpty)
|
||||
XCTAssertEqual(viewModel.uploadProgress, 100)
|
||||
}
|
||||
|
||||
/// 测试相册上传失败时不会登记文件入库。
|
||||
func testAlbumTrailerUploadFailureDoesNotRegisterFile() async {
|
||||
let api = MockAssetsService()
|
||||
let uploader = MockAssetsUploader()
|
||||
uploader.shouldFail = true
|
||||
let viewModel = AlbumTrailerViewModel()
|
||||
viewModel.selectedFolderId = 3
|
||||
viewModel.addLocalFiles([AlbumLocalUploadFile(data: Data([1]), fileName: "a.jpg", fileType: 2)])
|
||||
|
||||
let success = await viewModel.submit(scenicId: 9, api: api, uploadService: uploader)
|
||||
|
||||
XCTAssertFalse(success)
|
||||
XCTAssertEqual(api.albumUploadURLRequests.count, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// 资产服务测试替身,记录请求并返回预设数据。
|
||||
@ -164,8 +279,25 @@ private final class MockAssetsService: AssetsServing {
|
||||
let page: Int
|
||||
}
|
||||
|
||||
struct AlbumFolderListRequest: Equatable {
|
||||
let scenicId: Int
|
||||
let page: Int
|
||||
let pageSize: Int
|
||||
let name: String
|
||||
let startTime: String?
|
||||
let endTime: String?
|
||||
}
|
||||
|
||||
struct AlbumUploadURLRequest: Equatable {
|
||||
let scenicId: Int
|
||||
let fileURL: String
|
||||
let folderId: Int
|
||||
}
|
||||
|
||||
var cloudPages: [ListPayload<CloudDriveFile>] = [ListPayload(total: 0, list: [])]
|
||||
var mediaListResponses: [MediaLibraryListResponse] = [MediaLibraryListResponse.fixture(total: 0, ids: [])]
|
||||
var albumFolderPages: [ListPayload<AlbumFolderItem>] = [ListPayload(total: 0, list: [])]
|
||||
var albumFilePages: [ListPayload<AlbumFileItem>] = [ListPayload(total: 0, list: [])]
|
||||
var uploadPermission = true
|
||||
var uploadReason = ""
|
||||
var cloudListRequests: [CloudListRequest] = []
|
||||
@ -174,6 +306,13 @@ private final class MockAssetsService: AssetsServing {
|
||||
var mediaOperationRequests: [MediaAlbumOperationRequest] = []
|
||||
var mediaUploadRequests: [MediaAlbumUploadRequest] = []
|
||||
var mediaEditRequests: [MediaAlbumEditRequest] = []
|
||||
var albumFolderListRequests: [AlbumFolderListRequest] = []
|
||||
var albumInfoRequests: [Int] = []
|
||||
var albumFileListRequests: [(scenicId: Int, folderId: Int, fileType: Int, page: Int)] = []
|
||||
var albumAddRequests: [(scenicId: Int, name: String, remark: String)] = []
|
||||
var albumEditRequests: [AlbumFolderEditRequest] = []
|
||||
var albumDeleteRequests: [AlbumFileDeleteRequest] = []
|
||||
var albumUploadURLRequests: [AlbumUploadURLRequest] = []
|
||||
|
||||
/// 返回云盘文件列表。
|
||||
func cloudFileList(parentFolderId: Int, name: String, type: Int, orderBy: Int, page: Int, pageSize: Int) async throws -> ListPayload<CloudDriveFile> {
|
||||
@ -237,6 +376,44 @@ private final class MockAssetsService: AssetsServing {
|
||||
func mediaAlbumEdit(_ request: MediaAlbumEditRequest) async throws {
|
||||
mediaEditRequests.append(request)
|
||||
}
|
||||
|
||||
/// 返回相册文件夹列表。
|
||||
func albumFolderList(scenicId: Int, page: Int, pageSize: Int, name: String, startTime: String?, endTime: String?) async throws -> ListPayload<AlbumFolderItem> {
|
||||
albumFolderListRequests.append(AlbumFolderListRequest(scenicId: scenicId, page: page, pageSize: pageSize, name: name, startTime: startTime, endTime: endTime))
|
||||
return albumFolderPages.isEmpty ? ListPayload(total: 0, list: []) : albumFolderPages.removeFirst()
|
||||
}
|
||||
|
||||
/// 返回相册文件夹详情。
|
||||
func albumFolderInfo(id: Int) async throws -> AlbumFolderItem {
|
||||
albumInfoRequests.append(id)
|
||||
return AlbumFolderItem.fixture(id: id, name: "相册\(id)")
|
||||
}
|
||||
|
||||
/// 返回相册文件列表。
|
||||
func albumFileList(scenicId: Int, folderId: Int, fileType: Int, page: Int, pageSize: Int) async throws -> ListPayload<AlbumFileItem> {
|
||||
albumFileListRequests.append((scenicId: scenicId, folderId: folderId, fileType: fileType, page: page))
|
||||
return albumFilePages.isEmpty ? ListPayload(total: 0, list: []) : albumFilePages.removeFirst()
|
||||
}
|
||||
|
||||
/// 新建相册。
|
||||
func addAlbumFolder(scenicId: Int, name: String, remark: String) async throws {
|
||||
albumAddRequests.append((scenicId: scenicId, name: name, remark: remark))
|
||||
}
|
||||
|
||||
/// 编辑相册。
|
||||
func editAlbumFolder(folderId: Int, coverFileId: Int?, name: String?, remark: String?) async throws {
|
||||
albumEditRequests.append(AlbumFolderEditRequest(folderId: folderId, coverFileId: coverFileId, name: name, remark: remark))
|
||||
}
|
||||
|
||||
/// 删除相册文件。
|
||||
func deleteAlbumFiles(folderId: Int, idList: [Int]) async throws {
|
||||
albumDeleteRequests.append(AlbumFileDeleteRequest(idList: idList, folderId: folderId))
|
||||
}
|
||||
|
||||
/// 登记相册上传文件。
|
||||
func albumFileUploadURL(scenicId: Int, fileURL: String, folderId: Int) async throws {
|
||||
albumUploadURLRequests.append(AlbumUploadURLRequest(scenicId: scenicId, fileURL: fileURL, folderId: folderId))
|
||||
}
|
||||
}
|
||||
|
||||
/// OSS 上传测试替身,记录上传文件并返回固定 CDN URL。
|
||||
@ -362,3 +539,39 @@ private extension MediaLibraryDetail {
|
||||
return try! JSONDecoder().decode(MediaLibraryDetail.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
private extension AlbumFolderItem {
|
||||
/// 创建相册文件夹测试数据。
|
||||
static func fixture(id: Int, name: String) -> AlbumFolderItem {
|
||||
let payload: [String: Any] = [
|
||||
"id": id,
|
||||
"name": name,
|
||||
"cover_file_id": 0,
|
||||
"count_video": 1,
|
||||
"count_image": 2,
|
||||
"created_at": "2026",
|
||||
"remark": "备注"
|
||||
]
|
||||
let data = try! JSONSerialization.data(withJSONObject: payload)
|
||||
return try! JSONDecoder().decode(AlbumFolderItem.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
private extension AlbumFileItem {
|
||||
/// 创建相册文件测试数据。
|
||||
static func fixture(id: Int, fileType: Int) -> AlbumFileItem {
|
||||
let fileName = fileType == 1 ? "v.mp4" : "a.jpg"
|
||||
let payload: [String: Any] = [
|
||||
"id": id,
|
||||
"file_name": fileName,
|
||||
"file_type": fileType,
|
||||
"file_url": "https://cdn.example.com/\(fileName)",
|
||||
"cover_url": "",
|
||||
"file_size": "2048",
|
||||
"file_size_human": "2KB",
|
||||
"remark": ""
|
||||
]
|
||||
let data = try! JSONSerialization.data(withJSONObject: payload)
|
||||
return try! JSONDecoder().decode(AlbumFileItem.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user