Files
Sportstime/SportsTimeTests/Features/Trip/ItineraryRowFlatteningTests.swift

194 lines
9.7 KiB
Swift

//
// ItineraryRowFlatteningTests.swift
// SportsTimeTests
//
// Tests for row flattening order and ItineraryRowItem model.
//
import XCTest
@testable import SportsTime
private typealias H = ItineraryTestHelpers
@MainActor
final class ItineraryRowFlatteningTests: XCTestCase {
private let testDate = H.testDate
// MARK: - Row Flattening Order Tests
/// Verifies that rows are flattened in correct order under SEMANTIC TRAVEL MODEL:
/// 1. Day header
/// 2. Items with sortOrder < 0 (before games, including travel)
/// 3. Games
/// 4. Items with sortOrder >= 0 (after games, including travel)
///
/// NOTE: travelBefore is IGNORED - travel must be in items with sortOrder to appear.
func test_rowFlattening_correctOrder_semanticTravel() {
// Given: A day with travel in items (before games), games, and custom item (after games)
let travel = H.makeTravelSegment(from: "Chicago", to: "Detroit")
let travelItem = H.makeTravelItem(from: "Chicago", to: "Detroit", day: 1, sortOrder: -1.0)
let games = [H.makeRichGame(city: "Detroit", hour: 19)]
let customItem = H.makeCustomItem(day: 1, sortOrder: 1.0, title: "Dinner")
let dayData = ItineraryDayData(
id: 1,
dayNumber: 1,
date: testDate,
games: games,
items: [.travel(travel, dayNumber: 1), .customItem(customItem)],
travelBefore: nil // travelBefore is IGNORED under semantic model
)
// When: Controller reloads with travel having negative sortOrder (before games)
let controller = ItineraryTableViewController(style: .plain)
controller.reloadData(days: [dayData], travelValidRanges: [:], itineraryItems: [travelItem, customItem])
// Then: Order should be: header, travel (before games), games, custom (after games)
let rowCount = controller.tableView(controller.tableView, numberOfRowsInSection: 0)
XCTAssertEqual(rowCount, 4, "Expected 4 rows: header, travel, games, custom item")
// Verify order by reorderability
XCTAssertFalse(controller.tableView(controller.tableView, canMoveRowAt: IndexPath(row: 0, section: 0)), "Row 0 = Header (NOT reorderable)")
XCTAssertTrue(controller.tableView(controller.tableView, canMoveRowAt: IndexPath(row: 1, section: 0)), "Row 1 = Travel (reorderable)")
XCTAssertFalse(controller.tableView(controller.tableView, canMoveRowAt: IndexPath(row: 2, section: 0)), "Row 2 = Games (NOT reorderable)")
XCTAssertTrue(controller.tableView(controller.tableView, canMoveRowAt: IndexPath(row: 3, section: 0)), "Row 3 = Custom item (reorderable)")
}
func test_rowFlattening_itemsBeforeGames_negativeSortOrder() {
// Given: Custom items with negative sortOrder should appear BEFORE games
let games = [H.makeRichGame(city: "Detroit", hour: 19)]
let beforeItem = H.makeCustomItem(day: 1, sortOrder: -1.0, title: "Morning coffee")
let afterItem = H.makeCustomItem(day: 1, sortOrder: 1.0, title: "Dinner")
let dayData = ItineraryDayData(
id: 1,
dayNumber: 1,
date: testDate,
games: games,
items: [.customItem(beforeItem), .customItem(afterItem)],
travelBefore: nil
)
let controller = ItineraryTableViewController(style: .plain)
controller.reloadData(days: [dayData], travelValidRanges: [:], itineraryItems: [beforeItem, afterItem])
// Then: Order should be header, beforeItem, games, afterItem
let rowCount = controller.tableView(controller.tableView, numberOfRowsInSection: 0)
XCTAssertEqual(rowCount, 4, "Expected 4 rows: header, before-item, games, after-item")
// Verify the before-games item appears at row 1 (after header at row 0)
XCTAssertFalse(controller.tableView(controller.tableView, canMoveRowAt: IndexPath(row: 0, section: 0)), "Row 0 should be header (not reorderable)")
XCTAssertTrue(controller.tableView(controller.tableView, canMoveRowAt: IndexPath(row: 1, section: 0)), "Row 1 should be before-item (reorderable)")
XCTAssertFalse(controller.tableView(controller.tableView, canMoveRowAt: IndexPath(row: 2, section: 0)), "Row 2 should be games (not reorderable)")
XCTAssertTrue(controller.tableView(controller.tableView, canMoveRowAt: IndexPath(row: 3, section: 0)), "Row 3 should be after-item (reorderable)")
}
func test_rowFlattening_multipleItemsSortedBySortOrder() {
// Given: Multiple custom items should be sorted by sortOrder
let item1 = H.makeCustomItem(day: 1, sortOrder: 3.0, title: "Third")
let item2 = H.makeCustomItem(day: 1, sortOrder: 1.0, title: "First")
let item3 = H.makeCustomItem(day: 1, sortOrder: 2.0, title: "Second")
let dayData = ItineraryDayData(
id: 1,
dayNumber: 1,
date: testDate,
games: [],
items: [.customItem(item1), .customItem(item2), .customItem(item3)],
travelBefore: nil
)
let controller = ItineraryTableViewController(style: .plain)
controller.reloadData(days: [dayData], travelValidRanges: [:], itineraryItems: [item1, item2, item3])
// Then: Items should appear in sortOrder: First (1.0), Second (2.0), Third (3.0)
let rowCount = controller.tableView(controller.tableView, numberOfRowsInSection: 0)
XCTAssertEqual(rowCount, 4, "Expected 4 rows: header + 3 items")
}
// MARK: - Day Number Calculation Tests
func test_dayNumber_firstDayHeader_returnsDay1() {
// Given: A simple 3-day trip
let days = H.makeDays(count: 3)
let controller = ItineraryTableViewController(style: .plain)
controller.reloadData(days: days, travelValidRanges: [:])
// The first row should be Day 1 header
XCTAssertFalse(controller.tableView(controller.tableView, canMoveRowAt: IndexPath(row: 0, section: 0)), "First row should be header")
}
func test_dayNumber_rowAfterHeader_belongsToSameDay() {
// Given: A day with games
let games = [H.makeRichGame(city: "Detroit", hour: 19)]
let dayData = ItineraryDayData(id: 1, dayNumber: 1, date: testDate, games: games, items: [], travelBefore: nil)
let controller = ItineraryTableViewController(style: .plain)
controller.reloadData(days: [dayData], travelValidRanges: [:])
// Row 0 = header (Day 1), Row 1 = games (belongs to Day 1)
XCTAssertEqual(controller.tableView(controller.tableView, numberOfRowsInSection: 0), 2)
}
func test_dayNumber_travelRow_belongsToItsDay() {
// Given: Travel in Day 2's items (semantic model - travelBefore is ignored)
let travel = H.makeTravelSegment(from: "Chicago", to: "Detroit")
let travelItem = H.makeTravelItem(from: "Chicago", to: "Detroit", day: 2, sortOrder: 1.0)
let day1 = ItineraryDayData(id: 1, dayNumber: 1, date: testDate, games: [], items: [], travelBefore: nil)
let day2 = ItineraryDayData(
id: 2,
dayNumber: 2,
date: H.dayAfter(testDate),
games: [],
items: [.travel(travel, dayNumber: 2)],
travelBefore: nil // travelBefore is IGNORED under semantic model
)
let controller = ItineraryTableViewController(style: .plain)
controller.reloadData(days: [day1, day2], travelValidRanges: [:], itineraryItems: [travelItem])
// Row order: Day1 header, Day2 header, travel (in day 2's after-games region)
let rowCount = controller.tableView(controller.tableView, numberOfRowsInSection: 0)
XCTAssertEqual(rowCount, 3, "Expected: Day1 header, Day2 header, travel")
// Travel is reorderable and belongs to Day 2 (positioned after Day 2 header)
XCTAssertFalse(controller.tableView(controller.tableView, canMoveRowAt: IndexPath(row: 0, section: 0)), "Day 1 header")
XCTAssertFalse(controller.tableView(controller.tableView, canMoveRowAt: IndexPath(row: 1, section: 0)), "Day 2 header")
XCTAssertTrue(controller.tableView(controller.tableView, canMoveRowAt: IndexPath(row: 2, section: 0)), "Travel is reorderable")
}
// MARK: - ItineraryRowItem Tests
func test_itineraryRowItem_dayHeader_hasCorrectId() {
let item = ItineraryRowItem.dayHeader(dayNumber: 3, date: testDate)
XCTAssertEqual(item.id, "day:3")
}
func test_itineraryRowItem_games_hasCorrectId() {
let games = [H.makeRichGame(city: "Detroit", hour: 19)]
let item = ItineraryRowItem.games(games, dayNumber: 2)
XCTAssertEqual(item.id, "games:2")
}
func test_itineraryRowItem_travel_hasStableId() {
let segment = H.makeTravelSegment(from: "Chicago", to: "Detroit")
let item = ItineraryRowItem.travel(segment, dayNumber: 1)
XCTAssertTrue(item.id.hasPrefix("travel:"), "Travel ID should start with 'travel:'")
XCTAssertTrue(item.id.contains(segment.id.uuidString), "Travel ID should contain segment UUID")
}
func test_itineraryRowItem_customItem_hasUuidId() {
let customItem = H.makeCustomItem(day: 1, sortOrder: 1.0, title: "Test")
let item = ItineraryRowItem.customItem(customItem)
XCTAssertTrue(item.id.hasPrefix("item:"), "Custom item ID should start with 'item:'")
}
func test_itineraryRowItem_reorderability() {
XCTAssertFalse(ItineraryRowItem.dayHeader(dayNumber: 1, date: testDate).isReorderable)
XCTAssertFalse(ItineraryRowItem.games([], dayNumber: 1).isReorderable)
XCTAssertTrue(ItineraryRowItem.travel(H.makeTravelSegment(from: "A", to: "B"), dayNumber: 1).isReorderable)
XCTAssertTrue(ItineraryRowItem.customItem(H.makeCustomItem(day: 1, sortOrder: 1.0, title: "X")).isReorderable)
}
}