新增宣传页 Mock 模块并接入首页路由。

从 xh_test 同步 ScenicPromotion 演示页,补齐 MVVM 结构、本地资源、单元测试与模块文档,注册 promotion_page 首页入口。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-02 15:29:25 +08:00
parent a528d9b0d7
commit 1b1e93b9ca
30 changed files with 2661 additions and 4 deletions

View File

@ -0,0 +1,183 @@
//
// ScenicPromotionTests.swift
// suixinkanTests
//
// Created by Codex on 2026/7/2.
//
import XCTest
@testable import suixinkan
/// ViewModel
@MainActor
final class ScenicPromotionViewModelTests: XCTestCase {
/// normal
func testApplyNormalScenarioLoadsBoardAndSelection() {
let viewModel = ScenicPromotionViewModel()
viewModel.applyScenario(.normal)
XCTAssertEqual(viewModel.pageState, .normal)
XCTAssertNotNil(viewModel.board)
XCTAssertEqual(viewModel.board?.scenicName, "那拉提景区")
XCTAssertNotNil(viewModel.selectedSpot)
XCTAssertNotNil(viewModel.selectedMedia)
XCTAssertFalse(viewModel.mediaItems.isEmpty)
}
/// empty
func testApplyEmptyScenarioClearsBoard() {
let viewModel = ScenicPromotionViewModel()
viewModel.applyScenario(.normal)
viewModel.applyScenario(.empty)
XCTAssertEqual(viewModel.pageState, .empty)
XCTAssertNil(viewModel.board)
}
/// error
func testApplyErrorScenarioSetsErrorState() {
let viewModel = ScenicPromotionViewModel()
viewModel.applyScenario(.error)
if case .error = viewModel.pageState {
XCTAssertTrue(true)
} else {
XCTFail("Expected error page state")
}
}
///
func testNoMediaScenarioHidesMediaItems() {
let viewModel = ScenicPromotionViewModel()
viewModel.applyScenario(.normal)
viewModel.applyScenario(.noMedia)
XCTAssertTrue(viewModel.mediaItems.isEmpty)
XCTAssertNil(viewModel.selectedMedia)
}
///
func testNoQueueScenarioHidesQueueInfo() {
let viewModel = ScenicPromotionViewModel()
viewModel.applyScenario(.normal)
viewModel.applyScenario(.noQueue)
XCTAssertNil(viewModel.queue)
XCTAssertFalse(viewModel.hasQueueData)
}
///
func testSelectSpotAndModeUpdatesSelectedMedia() {
let viewModel = ScenicPromotionViewModel()
viewModel.applyScenario(.normal)
guard let board = viewModel.board, board.spots.count > 1 else {
XCTFail("Expected multiple spots in mock board")
return
}
let secondSpot = board.spots[1]
viewModel.selectSpot(secondSpot)
XCTAssertEqual(viewModel.selectedSpot?.id, secondSpot.id)
viewModel.selectMode(.photo)
XCTAssertEqual(viewModel.selectedMode, .photo)
XCTAssertEqual(viewModel.selectedMedia?.mode, .photo)
}
///
func testMoveMediaCyclesSelection() {
let viewModel = ScenicPromotionViewModel()
viewModel.applyScenario(.normal)
guard viewModel.mediaItems.count > 1 else {
XCTFail("Expected multiple media items")
return
}
let firstID = viewModel.selectedMedia?.id
viewModel.moveMedia(1)
XCTAssertNotEqual(viewModel.selectedMedia?.id, firstID)
viewModel.moveMedia(-1)
XCTAssertEqual(viewModel.selectedMedia?.id, firstID)
}
}
/// ViewModel
@MainActor
final class ScenicPromotionSettingsViewModelTests: XCTestCase {
/// Mock
func testApplyNormalScenarioLoadsSettingsData() {
let viewModel = ScenicPromotionSettingsViewModel()
viewModel.applyScenario(.normal)
XCTAssertEqual(viewModel.pageState, .normal)
XCTAssertFalse(viewModel.queuePoints.isEmpty)
XCTAssertFalse(viewModel.materialPoints.isEmpty)
XCTAssertFalse(viewModel.selectedQueuePointIDs.isEmpty)
}
///
func testAddMaterialFailsWithoutMaterialPoint() {
let viewModel = ScenicPromotionSettingsViewModel()
viewModel.applyScenario(.normal)
viewModel.selectedMaterialPointID = nil
XCTAssertFalse(viewModel.addMaterial(kind: .video))
}
/// Mock
func testAddMaterialSucceedsWithMaterialPoint() {
let viewModel = ScenicPromotionSettingsViewModel()
viewModel.applyScenario(.normal)
guard let point = viewModel.materialPoints.first else {
XCTFail("Expected material points")
return
}
viewModel.selectMaterialPoint(point)
let beforeCount = viewModel.materials(for: .video).count
XCTAssertTrue(viewModel.addMaterial(kind: .video))
XCTAssertEqual(viewModel.materials(for: .video).count, beforeCount + 1)
}
///
func testDeleteMaterialRemovesItem() {
let viewModel = ScenicPromotionSettingsViewModel()
viewModel.applyScenario(.normal)
guard let point = viewModel.materialPoints.first else {
XCTFail("Expected material points")
return
}
viewModel.selectMaterialPoint(point)
guard viewModel.addMaterial(kind: .image),
let material = viewModel.materials(for: .image).last else {
XCTFail("Expected uploaded material")
return
}
viewModel.deleteMaterial(material)
XCTAssertFalse(viewModel.materials(for: .image).contains(where: { $0.id == material.id }))
}
///
func testSubmitFailsWithoutQueuePoints() {
let viewModel = ScenicPromotionSettingsViewModel()
viewModel.applyScenario(.normal)
viewModel.selectedQueuePointIDs = []
XCTAssertFalse(viewModel.submit())
}
///
func testSubmitSucceedsWithQueuePoints() {
let viewModel = ScenicPromotionSettingsViewModel()
viewModel.applyScenario(.normal)
XCTAssertTrue(viewModel.submit())
}
}