Stabilize beta release with warning cleanup and edge-case fixes

This commit is contained in:
Trey t
2026-02-22 13:18:14 -06:00
parent fddea81e36
commit ec2bbb4764
55 changed files with 712 additions and 315 deletions

View File

@@ -229,6 +229,24 @@ struct PollVoteTests {
// Index 5 is ignored since it's >= tripCount
}
@Test("calculateScores: ignores negative trip indices")
func calculateScores_negativeIndex() {
let scores = PollVote.calculateScores(rankings: [0, -1, 1], tripCount: 3)
#expect(scores[0] == 3)
#expect(scores[1] == 1)
#expect(scores[2] == 0)
}
@Test("calculateScores: ignores duplicate trip indices after first occurrence")
func calculateScores_duplicateIndices() {
let scores = PollVote.calculateScores(rankings: [1, 1, 0], tripCount: 3)
#expect(scores[1] == 3)
#expect(scores[0] == 1)
#expect(scores[2] == 0)
}
@Test("calculateScores: empty rankings returns zeros")
func calculateScores_emptyRankings() {
let scores = PollVote.calculateScores(rankings: [], tripCount: 3)

View File

@@ -10,6 +10,7 @@ import XCTest
private typealias H = ItineraryTestHelpers
@MainActor
final class ItineraryCustomItemTests: XCTestCase {
private let testTripId = H.testTripId

View File

@@ -10,6 +10,7 @@ import XCTest
private typealias H = ItineraryTestHelpers
@MainActor
final class ItineraryEdgeCaseTests: XCTestCase {
private let testDate = H.testDate

View File

@@ -12,6 +12,7 @@ import XCTest
private typealias H = ItineraryTestHelpers
private typealias Logic = ItineraryReorderingLogic
@MainActor
final class ItineraryReorderingLogicTests: XCTestCase {
private let testDate = H.testDate
@@ -302,13 +303,8 @@ final class ItineraryReorderingLogicTests: XCTestCase {
func test_calculateSortOrder_emptyDay_returns1() {
// Day with only header, no items
let items = buildFlatItems([
.day(1),
.day(2)
])
// Simulating drop right after day 1 header (row 0)
// After inserting at row 1, day 1 has no other items
// Simulating drop right after day 1 header (row 0). After inserting
// at row 1, day 1 has no other items.
let mockItems = buildFlatItems([
.day(1),
.custom("New", sortOrder: 999, day: 1), // Placeholder for dropped item
@@ -810,16 +806,18 @@ final class ItineraryReorderingLogicTests: XCTestCase {
])
let customItem = H.makeCustomItem(day: 1, sortOrder: 1.0, title: "A")
let zones = Logic.calculateCustomItemDragZones(item: customItem, flatItems: items)
let zones = Logic.calculateCustomItemDragZones(
item: customItem,
sourceRow: 1,
flatItems: items,
constraints: nil,
findTravelSortOrder: { _ in nil }
)
// Headers at rows 0, 2, 4 should be invalid
XCTAssertTrue(zones.invalidRowIndices.contains(0))
XCTAssertTrue(zones.invalidRowIndices.contains(2))
XCTAssertTrue(zones.invalidRowIndices.contains(4))
// Items at rows 1, 3 should be valid
XCTAssertTrue(zones.validDropRows.contains(1))
XCTAssertTrue(zones.validDropRows.contains(3))
// New API excludes source row from both valid and invalid sets.
XCTAssertEqual(zones.invalidRowIndices, Set([0]))
XCTAssertEqual(Set(zones.validDropRows), Set([2, 3, 4, 5]))
XCTAssertFalse(zones.validDropRows.contains(1))
}
func test_calculateTravelDragZones_respectsDayRange() {
@@ -834,17 +832,21 @@ final class ItineraryReorderingLogicTests: XCTestCase {
let segment = H.makeTravelSegment(from: "CityA", to: "CityB")
let travelValidRanges = ["travel:0:citya->cityb": 1...3]
let travelItem = H.makeTravelItem(from: "CityA", to: "CityB", day: 2, sortOrder: 1.0)
let zones = Logic.calculateTravelDragZones(
segment: segment,
sourceRow: 3,
flatItems: items,
travelValidRanges: travelValidRanges,
constraints: nil,
findTravelItem: { _ in nil }
findTravelItem: { _ in travelItem },
makeTravelItem: { _ in travelItem },
findTravelSortOrder: { _ in travelItem.sortOrder }
)
// All days 1-3 should be valid (6 rows total)
XCTAssertEqual(zones.validDropRows.count, 6)
XCTAssertTrue(zones.invalidRowIndices.isEmpty)
XCTAssertEqual(Set(zones.validDropRows), Set([1, 2, 4, 5, 6]))
XCTAssertEqual(zones.invalidRowIndices, Set([0]))
XCTAssertFalse(zones.validDropRows.contains(3))
}
}

View File

@@ -10,6 +10,7 @@ import XCTest
private typealias H = ItineraryTestHelpers
@MainActor
final class ItineraryReorderingTests: XCTestCase {
private let testDate = H.testDate

View File

@@ -10,6 +10,7 @@ import XCTest
private typealias H = ItineraryTestHelpers
@MainActor
final class ItineraryRowFlatteningTests: XCTestCase {
private let testDate = H.testDate

View File

@@ -10,6 +10,7 @@ import XCTest
private typealias H = ItineraryTestHelpers
@MainActor
final class ItinerarySortOrderTests: XCTestCase {
private let testDate = H.testDate

View File

@@ -10,6 +10,7 @@ import XCTest
private typealias H = ItineraryTestHelpers
@MainActor
final class ItineraryTravelConstraintTests: XCTestCase {
private let testTripId = H.testTripId
@@ -176,10 +177,6 @@ final class ItineraryTravelConstraintTests: XCTestCase {
)
let controller = ItineraryTableViewController(style: .plain)
var capturedSortOrder: Double = 0
controller.onTravelMoved = { _, _, sortOrder in
capturedSortOrder = sortOrder
}
controller.reloadData(
days: [dayData],
travelValidRanges: ["travel:0:chicago->detroit": 1...1],

View File

@@ -13,6 +13,7 @@ import Testing
@testable import SportsTime
@Suite("RegionMapSelector — regionForCoordinate")
@MainActor
struct RegionMapSelectorTests {
// MARK: - West Region (longitude < -102)

View File

@@ -10,6 +10,7 @@
import XCTest
@testable import SportsTime
@MainActor
final class TravelPlacementTests: XCTestCase {
// MARK: - Helpers
@@ -417,14 +418,14 @@ final class TravelPlacementTests: XCTestCase {
// Follow Team pattern: HoustonCincinnatiHoustonCincinnati
// Two segments share the same city pair (HoustonCincinnati)
// Each must get a unique travel anchor ID so overrides don't collide.
let stops = [
_ = [
makeStop(city: "Houston", arrival: may(1), departure: may(2)), // Stop 0
makeStop(city: "Cincinnati", arrival: may(4), departure: may(5)), // Stop 1
makeStop(city: "Houston", arrival: may(7), departure: may(8)), // Stop 2
makeStop(city: "Cincinnati", arrival: may(10), departure: may(11)) // Stop 3
]
let segments = [
_ = [
makeSegment(from: "Houston", to: "Cincinnati"), // Seg 0: stops[0] stops[1]
makeSegment(from: "Cincinnati", to: "Houston"), // Seg 1: stops[1] stops[2]
makeSegment(from: "Houston", to: "Cincinnati") // Seg 2: stops[2] stops[3]

View File

@@ -235,7 +235,7 @@ struct GameDAGRouterTests {
let (game1, stadium1) = makeGameAndStadium(city: "New York", date: dates[0], coord: nycCoord)
let (game2, stadium2) = makeGameAndStadium(city: "Boston", date: dates[1], coord: bostonCoord)
let (game3, stadium3) = makeGameAndStadium(city: "New York", date: dates[2], coord: nycCoord) // Same city
let (game3, _) = makeGameAndStadium(city: "New York", date: dates[2], coord: nycCoord) // Same city
// Use same stadium ID for same city
let nyStadium = stadium1

View File

@@ -73,7 +73,6 @@ struct Bug2_InfiniteLoopTests {
@Test("calculateRestDays does not hang for normal multi-day stop")
func calculateRestDays_normalMultiDay_terminates() {
let calendar = TestClock.calendar
let arrival = TestFixtures.date(year: 2026, month: 6, day: 10, hour: 12)
let departure = TestFixtures.date(year: 2026, month: 6, day: 14, hour: 12)

View File

@@ -257,6 +257,17 @@ struct RouteFiltersTests {
#expect(result.count == 1)
}
@Test("filterByDateRange: inverted range bounds are normalized")
func filterByDateRange_invertedBounds_normalized() {
let dayAfterTomorrow = calendar.date(byAdding: .day, value: 2, to: today)!
let trip = makeTrip(startDate: tomorrow, endDate: dayAfterTomorrow)
// Callers can accidentally pass (end, start); filter should still behave as expected.
let result = RouteFilters.filterByDateRange([trip], start: nextWeek, end: today)
#expect(result.count == 1)
}
// MARK: - Specification Tests: filterByStatus
@Test("filterByStatus: returns matching status only")
@@ -369,15 +380,24 @@ struct RouteFiltersTests {
// MARK: - Edge Case Tests
@Test("Edge: city names are case-sensitive")
func edge_cityNamesCaseSensitive() {
@Test("Edge: city names are normalized case-insensitively")
func edge_cityNamesCaseInsensitive() {
let option = makeOption(stops: [
makeStop(city: "New York", date: today),
makeStop(city: "new york", date: tomorrow) // Different case
])
// Currently case-sensitive, so these are different cities
#expect(!RouteFilters.hasRepeatCityViolation(option))
#expect(RouteFilters.hasRepeatCityViolation(option))
}
@Test("Edge: city names are normalized for punctuation and spacing")
func edge_cityNamesNormalizedForPunctuationAndSpacing() {
let option = makeOption(stops: [
makeStop(city: "St. Louis", date: today),
makeStop(city: "st louis", date: tomorrow)
])
#expect(RouteFilters.hasRepeatCityViolation(option))
}
@Test("Edge: very long trip with many stops")

View File

@@ -48,6 +48,14 @@ struct PollErrorTests {
#expect(error.errorDescription!.lowercased().contains("owner"))
}
/// - Expected Behavior: notVoteOwner explains vote ownership requirement
@Test("errorDescription: notVoteOwner mentions owner")
func errorDescription_notVoteOwner() {
let error = PollError.notVoteOwner
#expect(error.errorDescription != nil)
#expect(error.errorDescription!.lowercased().contains("owner"))
}
/// - Expected Behavior: networkUnavailable explains connection issue
@Test("errorDescription: networkUnavailable mentions connection")
func errorDescription_networkUnavailable() {
@@ -83,6 +91,7 @@ struct PollErrorTests {
.pollNotFound,
.alreadyVoted,
.notPollOwner,
.notVoteOwner,
.networkUnavailable,
.encodingError,
.unknown(NSError(domain: "", code: 0))
@@ -102,6 +111,7 @@ struct PollErrorTests {
.pollNotFound,
.alreadyVoted,
.notPollOwner,
.notVoteOwner,
.networkUnavailable,
.encodingError
]