Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
4bb3772
🐛 Fix Swift local-first loading states
andrew-bierman Jul 15, 2026
e1d6d07
🧪 Harden Swift E2E screenshot coverage
andrew-bierman Jul 15, 2026
9c9baa1
🧱 Bound Swift CI smoke runtime
andrew-bierman Jul 15, 2026
2da5e8b
🧱 Pin Swift CI simulator destination
andrew-bierman Jul 15, 2026
1431139
🐛 Address Swift E2E review feedback
andrew-bierman Jul 15, 2026
c3a42f0
🔐 Align Swift release signing
andrew-bierman Jul 16, 2026
e30b655
🏷️ Label Swift beta builds
andrew-bierman Jul 16, 2026
9d1e58a
🏷️ Prepare Swift beta TestFlight assets
andrew-bierman Jul 16, 2026
bb2975c
🧪 Stabilize Swift E2E API fixtures
andrew-bierman Jul 17, 2026
1eb6334
🏷️ Add Swift deployed test controls
andrew-bierman Jul 17, 2026
e6b178f
✨ Polish Swift beta app coverage
andrew-bierman Jul 17, 2026
9348804
🔀 chore: sync development into swift beta branch
andrew-bierman Jul 18, 2026
eaeffcf
🧪 test: require real auth for deployed Swift validation
andrew-bierman Jul 18, 2026
1f982db
🧹 chore: satisfy PR checks for Swift validation
andrew-bierman Jul 18, 2026
ed5d651
🧹 chore: use env shim in Swift scripts
andrew-bierman Jul 18, 2026
c79a2ad
🧹 chore: remove Swift validation casts
andrew-bierman Jul 18, 2026
96f9593
🧹 chore: satisfy owned api lint
andrew-bierman Jul 18, 2026
02d8c7f
🧹 chore: type e2e api env keys
andrew-bierman Jul 18, 2026
6157e1d
🧪 test: run iOS smoke through Swift wrapper
andrew-bierman Jul 18, 2026
50a875f
🧪 test: default Swift CI local auth creds
andrew-bierman Jul 18, 2026
ec46ac2
🧪 test: make Swift iOS smoke self-contained in CI
andrew-bierman Jul 18, 2026
3bd847e
🐇 fix: address Swift beta review feedback
andrew-bierman Jul 18, 2026
3dac6de
🐇 fix: handle Swift runner spawn failures
andrew-bierman Jul 18, 2026
a346e1e
🐛 fix: await trip delete navigation
andrew-bierman Jul 18, 2026
d95fa33
🐇 fix: give iOS smoke CI headroom
andrew-bierman Jul 18, 2026
8b643a5
🧪 test: guard Swift TestFlight lanes
andrew-bierman Jul 18, 2026
364e32b
🧪 test: add Swift TestFlight dry run
andrew-bierman Jul 18, 2026
1a2049f
🐛 fix: locate exported TestFlight ipa
andrew-bierman Jul 18, 2026
cc72f42
🐛 fix: restore Expo sessions in Swift
andrew-bierman Jul 18, 2026
b2c9fb0
📦 add Swift App Store metadata
andrew-bierman Jul 18, 2026
69545fc
⌚ fix: embed Swift watch companion
andrew-bierman Jul 18, 2026
aaebe11
🎨 format Swift watch scripts
andrew-bierman Jul 18, 2026
dd0e351
✅ fail Swift smoke on test failures
andrew-bierman Jul 18, 2026
50ace55
⬆️ bump Nuke for Swift 6
andrew-bierman Jul 18, 2026
29bc5c6
✅ stabilize seeded Swift login
andrew-bierman Jul 18, 2026
fac932f
👷 align macOS smoke signing
andrew-bierman Jul 18, 2026
2172fba
⬆️ pin Nuke for Xcode 16 CI
andrew-bierman Jul 18, 2026
92410e5
🚀 harden TestFlight upload preflight
andrew-bierman Jul 18, 2026
2d66d21
📦 honor Swift archive build numbers
andrew-bierman Jul 18, 2026
730ab56
🎨 format TestFlight upload test
andrew-bierman Jul 18, 2026
02c6c86
👷 install xcodegen for Swift E2E
andrew-bierman Jul 18, 2026
19ff597
👷 boot simulator for Swift E2E
andrew-bierman Jul 18, 2026
5d6f491
👷 split Swift E2E test depth
andrew-bierman Jul 18, 2026
86d6cd6
👷 add Swift TestFlight sanity plan
andrew-bierman Jul 18, 2026
5562004
🧪 harden Swift login UI submit
andrew-bierman Jul 18, 2026
d2b3f61
🧪 stabilize iOS login submit
andrew-bierman Jul 18, 2026
bcafd75
🧪 submit Swift login via keyboard
andrew-bierman Jul 18, 2026
663ba09
🧪 surface Swift auth failures
andrew-bierman Jul 18, 2026
2f46c27
👷 preflight Swift deployed auth
andrew-bierman Jul 18, 2026
5a62958
🔧 route Swift auth preflight env
andrew-bierman Jul 18, 2026
d014636
🎨 format Swift auth preflight tests
andrew-bierman Jul 18, 2026
8773694
🧹 satisfy Swift auth preflight lint
andrew-bierman Jul 18, 2026
760d16f
🧹 remove Swift auth preflight cast
andrew-bierman Jul 18, 2026
e659327
🧪 harden Swift login submit tests
andrew-bierman Jul 18, 2026
3108b38
🚦 gate Swift replacement TestFlight uploads
andrew-bierman Jul 18, 2026
086324d
👷 add Swift TestFlight preflight workflow
andrew-bierman Jul 18, 2026
1f845ab
🔗 route Swift deep links
andrew-bierman Jul 18, 2026
fd1e14b
🚩 gate Swift OAuth buttons
andrew-bierman Jul 18, 2026
344f46a
🩺 clarify Swift deployed auth failures
andrew-bierman Jul 18, 2026
3b4ebcb
🚦 enforce Swift replacement upload preflight
andrew-bierman Jul 19, 2026
2cc1e67
🚦 harden Swift TestFlight verification
andrew-bierman Jul 19, 2026
93240fa
🔧 normalize blank E2E API env
andrew-bierman Jul 19, 2026
1bc08ed
🧪 run Swift PR sanity on deployed dev
andrew-bierman Jul 19, 2026
e5a1f80
🧹 satisfy TestFlight verifier lint
andrew-bierman Jul 19, 2026
f1ef84b
🔧 use safe guards in TestFlight verifier
andrew-bierman Jul 19, 2026
5573217
🧪 harden iOS login submit helper
andrew-bierman Jul 19, 2026
39f9e91
🧪 preserve E2E bootstrap failure output
andrew-bierman Jul 19, 2026
66600bc
🧪 harden TestFlight replacement verification
andrew-bierman Jul 19, 2026
042d554
🧪 extend iOS sanity test allowance
andrew-bierman Jul 19, 2026
6609064
🐛 fix Swift beta versioning and weather fallback
andrew-bierman Jul 21, 2026
c0f223e
Merge remote-tracking branch 'origin/development' into codex/swift-be…
andrew-bierman Jul 21, 2026
2bd180c
🧪 sync Swift feature flag config
andrew-bierman Jul 21, 2026
6b167e1
fix(swift): address tester navigation and units feedback
andrew-bierman Jul 23, 2026
cc2bc69
fix(ci): use dev API for scheduled Swift E2E
andrew-bierman Jul 23, 2026
766b726
🧪 test(api): harden local e2e launch readiness
andrew-bierman Aug 7, 2026
549f5b1
🧪 test(swift): harden visual screenshot coverage
andrew-bierman Aug 7, 2026
b50a9e8
🧹 chore(api): satisfy readiness lint gates
andrew-bierman Aug 7, 2026
2c4dd69
🧪 test(api): support local sql row results
andrew-bierman Aug 7, 2026
627f262
🐛 fix(swift): tolerate catalog items without weights
andrew-bierman Aug 7, 2026
6e35ad4
🚚 chore(swift): default to replacement app identity
andrew-bierman Aug 8, 2026
8c82ea6
🧪 test(swift): harden launch QA evidence
andrew-bierman Aug 8, 2026
a422f12
💄 style(swift): tighten responsive row polish
andrew-bierman Aug 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ struct PacksListView: View {

var body: some View {
Group {
if viewModel.isLoading && viewModel.packs.isEmpty && !isExplore {
if viewModel.isLoading && viewModel.packs.isEmpty && !viewModel.isCacheLoaded && !isExplore {
ProgressView("Loading packs…").frame(maxWidth: .infinity, maxHeight: .infinity)
} else if let error = viewModel.error, viewModel.packs.isEmpty, !isExplore {
ErrorView(error, retry: { await viewModel.load(context: modelContext) })
Expand Down Expand Up @@ -64,7 +64,6 @@ struct PacksListView: View {
Button("New Pack", systemImage: "plus") { showingCreateSheet = true }
.accessibilityIdentifier("packs_new_pack_button")
.keyboardShortcut("n", modifiers: .command)
.accessibilityIdentifier("new_pack_button")
}
if viewModel.isLoading || isLoadingPublic {
ProgressView().controlSize(.small)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ final class PacksViewModel {
let cachedPacks = cached.compactMap { $0.toPack() }
if !cachedPacks.isEmpty {
packs = cachedPacks
isCacheLoaded = true
}
isCacheLoaded = true
}

isLoading = packs.isEmpty
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ struct TripsListView: View {

var body: some View {
Group {
if viewModel.isLoading && viewModel.trips.isEmpty {
if viewModel.isLoading && viewModel.trips.isEmpty && !viewModel.isCacheLoaded {
ProgressView("Loading trips…").frame(maxWidth: .infinity, maxHeight: .infinity)
} else if let error = viewModel.error, viewModel.trips.isEmpty {
ErrorView(error, retry: { await viewModel.load(context: modelContext) })
Expand All @@ -42,7 +42,6 @@ struct TripsListView: View {
Button("Plan Trip", systemImage: "plus") { showingCreateSheet = true }
.accessibilityIdentifier("trips_plan_trip_button")
.keyboardShortcut("n", modifiers: [.command, .shift])
.accessibilityIdentifier("plan_trip_button")
}
}
.task { await viewModel.load(context: modelContext) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ final class TripsViewModel {
let cachedTrips = cached.compactMap { $0.toTrip() }
if !cachedTrips.isEmpty {
trips = cachedTrips
isCacheLoaded = true
}
isCacheLoaded = true
}

isLoading = trips.isEmpty
Expand Down
10 changes: 4 additions & 6 deletions apps/swift/Sources/PackRat/PackRatApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ struct PackRatApp: App {
#endif

init() {
// Telemetry has to start before any view is mounted so launch-time
// errors are captured. A missing DSN silently disables the SDK.
SentryConfig.start()

#if os(macOS)
if ProcessInfo.processInfo.arguments.contains("--reset-auth") {
UserDefaults.standard.set(true, forKey: "ApplePersistenceIgnoreState")
Expand All @@ -20,12 +24,6 @@ struct PackRatApp: App {
#endif
}

init() {
// Telemetry has to start before any view is mounted so launch-time
// errors are captured. A missing DSN silently disables the SDK.
SentryConfig.start()
}

var body: some Scene {
WindowGroup {
AuthGateView()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ final class PersistenceController {
let schema = Schema([CachedPack.self, CachedTrip.self, ShoppingItem.self])
let config = ModelConfiguration("PackRat", schema: schema)
do {
try FileManager.default.createDirectory(
at: FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0],
withIntermediateDirectories: true
)
container = try ModelContainer(for: schema, configurations: config)
} catch {
fatalError("SwiftData container failed: \(error)")
Expand Down
27 changes: 22 additions & 5 deletions apps/swift/Tests/PackRatUITests/HomeTileTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,19 @@ final class HomeTileTests: AppUITestCase {

func testEveryHomeNavigationTileOpensDestination() {
for tile in navigationTiles {
goToTab("Home")
goHome()
tapHomeTile(tile.id)

guard let destinationTitle = tile.destinationTitle else { continue }
XCTAssertTrue(
app.navigationBars[destinationTitle].waitForExistence(timeout: 8),
destinationExists(destinationTitle, timeout: 8),
"\(tile.id) must open \(destinationTitle)"
)
}
}

func testSeasonSuggestionsTileOpensAndDismissesSheet() {
goToTab("Home")
goHome()
tapHomeTile("home_tile_season_suggestions")

XCTAssertTrue(
Expand All @@ -46,7 +46,7 @@ final class HomeTileTests: AppUITestCase {
}

func testShoppingListTileSupportsAddToggleClearAndDone() {
goToTab("Home")
goHome()
tapHomeTile("home_tile_shopping_list")

XCTAssertTrue(
Expand Down Expand Up @@ -86,7 +86,24 @@ final class HomeTileTests: AppUITestCase {
)

waitFor(app.buttons["shopping_done"], timeout: 5).tap()
XCTAssertTrue(app.navigationBars["Home"].waitForExistence(timeout: 5))
XCTAssertTrue(destinationExists("Home", timeout: 5))
}

private func goHome() {
#if os(macOS)
goToSidebar("Home")
#else
goToTab("Home")
#endif
}

private func destinationExists(_ title: String, timeout: TimeInterval) -> Bool {
#if os(macOS)
return app.staticTexts[title].waitForExistence(timeout: timeout)
|| app.navigationBars[title].waitForExistence(timeout: 1)
#else
return app.navigationBars[title].waitForExistence(timeout: timeout)
#endif
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

private func tapHomeTile(_ id: String) {
Expand Down
12 changes: 10 additions & 2 deletions apps/swift/Tests/PackRatUITests/ScreenshotSmokeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ final class ScreenshotSmokeTests: AppUITestCase {
func testCaptureCoreScreens() throws {
capture("02-home")

goToTab("Packs")
goToDestination("Packs")
XCTAssertTrue(app.navigationBars["Packs"].waitForExistence(timeout: 8))
capture("03-packs")

goToTab("Weather")
goToDestination("Weather")
XCTAssertTrue(app.navigationBars["Weather"].waitForExistence(timeout: 8))
let searchField = app.textFields["Search locations..."].exists
? app.textFields["Search locations..."]
Expand All @@ -40,4 +40,12 @@ final class ScreenshotSmokeTests: AppUITestCase {
attachment.lifetime = .keepAlways
add(attachment)
}

private func goToDestination(_ label: String) {
#if os(macOS)
goToSidebar(label)
#else
goToTab(label)
#endif
}
}
74 changes: 56 additions & 18 deletions apps/swift/Tests/PackRatUITests/VisualScreenshotTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -583,10 +583,26 @@ final class VisualScreenshotTests: XCTestCase {
openHomeForActionBaseline(name: baselineName)

let identifier = "home_action_\(title.lowercased().filter { $0.isLetter || $0.isNumber })"
if prefersHomeSearch(for: title) {
openHomeActionUsingSearch(title: title, identifier: identifier)
if let destinationIdentifier {
let destination = app.descendants(matching: .any).matching(identifier: destinationIdentifier).firstMatch
XCTAssertTrue(
destination.waitForExistence(timeout: 5),
"Expected Home action '\(title)' to open '\(destinationIdentifier)' for screenshot \(name)"
)
}
capture(name)
if dismissAfterCapture {
dismissPhoneDestination()
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return
}

let action = app.buttons[identifier]
var visibleCandidate: XCUIElement?

for _ in 0..<30 {
for _ in 0..<8 {
if action.exists {
visibleCandidate = action
}
Expand Down Expand Up @@ -641,6 +657,17 @@ final class VisualScreenshotTests: XCTestCase {
}
}

private func prefersHomeSearch(for title: String) -> Bool {
[
"AI Packs",
"Community Feed",
"Gear Inventory",
"Pack Templates",
"Trail Conditions",
"Wildlife ID",
].contains(title)
}

private func openHomeActionUsingSearch(title: String, identifier: String) {
resetActiveHomeSearchPresentation()

Expand Down Expand Up @@ -1039,6 +1066,7 @@ final class VisualScreenshotTests: XCTestCase {
) {
let element = app.descendants(matching: .any).matching(identifier: identifier).firstMatch
XCTAssertTrue(element.waitForExistence(timeout: 5), "Expected element identifier '\(identifier)' for screenshot \(name)")
XCTAssertTrue(element.isHittable, "Expected element identifier '\(identifier)' to be hittable for screenshot \(name)")
activate(element)
capture(name)
if dismissAfterCapture {
Expand All @@ -1048,7 +1076,7 @@ final class VisualScreenshotTests: XCTestCase {

private func scrollToElement(identifier: String, maxSwipes: Int = 5) {
let element = app.descendants(matching: .any).matching(identifier: identifier).firstMatch
for _ in 0..<maxSwipes where !element.exists {
for _ in 0..<maxSwipes where !element.exists || !element.isHittable {
app.swipeUp()
}
}
Expand All @@ -1060,7 +1088,7 @@ final class VisualScreenshotTests: XCTestCase {
}
activate(button)
capture(name)
dismissPresentedSurface()
dismissTransientOverlay()
}

private func openContextMenuAndCapture(identifier: String, name: String) {
Expand All @@ -1072,15 +1100,15 @@ final class VisualScreenshotTests: XCTestCase {
element.press(forDuration: 1)
#endif
capture(name)
dismissPresentedSurface()
dismissTransientOverlay()
}

private func tapAndCapture(identifier: String, name: String) {
guard let button = findButton(identifier: identifier, timeout: 3) else {
XCTFail("Expected button identifier '\(identifier)' for screenshot \(name)")
guard let element = findElement(identifier: identifier, timeout: 3) else {
XCTFail("Expected tappable element identifier '\(identifier)' for screenshot \(name)")
return
}
activate(button)
activate(element)
capture(name)
dismissPresentedSurface()
}
Expand All @@ -1091,12 +1119,12 @@ final class VisualScreenshotTests: XCTestCase {
name: String,
dismissAfterCapture: Bool = true
) {
let button = findButton(identifier: identifier, timeout: 1) ?? findButton(label: label, timeout: 3)
guard let button else {
XCTFail("Expected button identifier '\(identifier)' or label '\(label)' for screenshot \(name)")
let element = findElement(identifier: identifier, timeout: 1) ?? findButton(label: label, timeout: 3)
guard let element else {
XCTFail("Expected tappable element identifier '\(identifier)' or button label '\(label)' for screenshot \(name)")
return
}
activate(button)
activate(element)
capture(name)
if dismissAfterCapture {
dismissPresentedSurface()
Expand All @@ -1120,6 +1148,11 @@ final class VisualScreenshotTests: XCTestCase {
return findConcreteElement(in: query, timeout: timeout)
}

private func findElement(identifier: String, timeout: TimeInterval) -> XCUIElement? {
let query = app.descendants(matching: .any).matching(identifier: identifier)
return findConcreteElement(in: query, timeout: timeout)
}

private func findConcreteElement(in query: XCUIElementQuery, timeout: TimeInterval) -> XCUIElement? {
guard query.firstMatch.waitForExistence(timeout: timeout) else { return nil }
for element in query.allElementsBoundByIndex where element.exists && element.isHittable {
Expand Down Expand Up @@ -1166,6 +1199,14 @@ final class VisualScreenshotTests: XCTestCase {
#endif
}

private func dismissTransientOverlay() {
#if os(macOS)
app.typeKey(XCUIKeyboardKey.escape.rawValue, modifierFlags: [])
#else
app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.22)).tap()
#endif
}

private func launchAuthenticated(sampleData: Bool = false, forceOffline: Bool = false) {
let bundle = Bundle(for: VisualScreenshotTests.self)
let email = (bundle.object(forInfoDictionaryKey: "PACKRAT_E2E_EMAIL") as? String)
Expand Down Expand Up @@ -1325,13 +1366,10 @@ final class VisualScreenshotTests: XCTestCase {
private func dismissInterruption(in container: XCUIElement) -> Bool {
#if os(macOS)
for label in ["Remind Me Later", "Not Now", "Continue", "OK", "Allow", "Dismiss", "Close"] {
let button = container.buttons[label]
if button.exists {
if button.isHittable {
button.click()
} else {
button.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).click()
}
let matchingButtons = container.buttons.allElementsBoundByIndex
.filter { $0.label == label || $0.identifier == label }
if let button = matchingButtons.first(where: { $0.exists && $0.isHittable }) ?? matchingButtons.first(where: { $0.exists }) {
button.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).click()
return true
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand Down
14 changes: 14 additions & 0 deletions apps/swift/scripts/__tests__/args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ describe('parseArgs', () => {
expect(parseArgs(['--plan=smoke']).plan).toBe('iOS-Smoke');
});

it('accepts legacy positional iOS mode aliases used by package scripts', () => {
expect(parseArgs(['ios-ui'])).toEqual({ plan: 'iOS-Full', passthrough: [] });
expect(parseArgs(['ios-smoke'])).toEqual({ plan: 'iOS-Smoke', passthrough: [] });
});

it('maps unit mode to the iOS unit test target instead of an xcodebuild action', () => {
expect(parseArgs(['unit'])).toEqual({
passthrough: ['-only-testing:PackRatTests'],
});
expect(parseArgs(['ios-unit'])).toEqual({
passthrough: ['-only-testing:PackRatTests'],
});
});

it('case-insensitive alias matching', () => {
expect(parseArgs(['--plan', 'SMOKE']).plan).toBe('iOS-Smoke');
expect(parseArgs(['--plan', 'FULL']).plan).toBe('iOS-Full');
Expand Down
Loading
Loading