Stabilize beta release with warning cleanup and edge-case fixes
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -10,6 +10,7 @@ import XCTest
|
||||
|
||||
private typealias H = ItineraryTestHelpers
|
||||
|
||||
@MainActor
|
||||
final class ItineraryCustomItemTests: XCTestCase {
|
||||
|
||||
private let testTripId = H.testTripId
|
||||
|
||||
@@ -10,6 +10,7 @@ import XCTest
|
||||
|
||||
private typealias H = ItineraryTestHelpers
|
||||
|
||||
@MainActor
|
||||
final class ItineraryEdgeCaseTests: XCTestCase {
|
||||
|
||||
private let testDate = H.testDate
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import XCTest
|
||||
|
||||
private typealias H = ItineraryTestHelpers
|
||||
|
||||
@MainActor
|
||||
final class ItineraryReorderingTests: XCTestCase {
|
||||
|
||||
private let testDate = H.testDate
|
||||
|
||||
@@ -10,6 +10,7 @@ import XCTest
|
||||
|
||||
private typealias H = ItineraryTestHelpers
|
||||
|
||||
@MainActor
|
||||
final class ItineraryRowFlatteningTests: XCTestCase {
|
||||
|
||||
private let testDate = H.testDate
|
||||
|
||||
@@ -10,6 +10,7 @@ import XCTest
|
||||
|
||||
private typealias H = ItineraryTestHelpers
|
||||
|
||||
@MainActor
|
||||
final class ItinerarySortOrderTests: XCTestCase {
|
||||
|
||||
private let testDate = H.testDate
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -13,6 +13,7 @@ import Testing
|
||||
@testable import SportsTime
|
||||
|
||||
@Suite("RegionMapSelector — regionForCoordinate")
|
||||
@MainActor
|
||||
struct RegionMapSelectorTests {
|
||||
|
||||
// MARK: - West Region (longitude < -102)
|
||||
|
||||
@@ -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: Houston→Cincinnati→Houston→Cincinnati
|
||||
// Two segments share the same city pair (Houston→Cincinnati)
|
||||
// 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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user