Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
100 changes: 100 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,110 @@ jobs:
CODE_SIGNING_ALLOWED=NO \
-resultBundlePath TestResults.xcresult

- name: Check domain coverage
run: ./scripts/check-domain-coverage.sh TestResults.xcresult 80

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: TestResults.xcresult
retention-days: 7

companions:
name: Companion apps
runs-on: macos-15

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install xcodegen
run: brew install xcodegen

- name: Generate Xcode project
run: xcodegen

- name: Build iPhone app
run: |
xcodebuild build \
-project Nightcap.xcodeproj \
-scheme NightcapPhone \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-skipMacroValidation \
CODE_SIGNING_ALLOWED=NO

- name: Build watch app
run: |
xcodebuild build \
-project Nightcap.xcodeproj \
-scheme NightcapWatch \
-destination 'generic/platform=watchOS Simulator' \
-skipMacroValidation \
CODE_SIGNING_ALLOWED=NO

- name: Snapshot tests
run: |
xcodebuild test \
-project Nightcap.xcodeproj \
-scheme NightcapPhone \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-skipMacroValidation \
CODE_SIGNING_ALLOWED=NO \
-resultBundlePath PhoneTestResults.xcresult

- name: Upload phone test results
if: always()
uses: actions/upload-artifact@v4
with:
name: phone-test-results
path: PhoneTestResults.xcresult
retention-days: 7

e2e:
name: Maestro e2e
runs-on: macos-15

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install tools
run: |
brew install xcodegen
curl -Ls https://get.maestro.mobile.dev | bash
echo "$HOME/.maestro/bin" >> $GITHUB_PATH

- name: Generate Xcode project
run: xcodegen

- name: Boot simulator
run: |
UDID=$(xcrun simctl create maestro-iphone \
com.apple.CoreSimulator.SimDeviceType.iPhone-16)
xcrun simctl boot "$UDID"
echo "SIM_UDID=$UDID" >> $GITHUB_ENV

- name: Build and install app
run: |
xcodebuild build \
-project Nightcap.xcodeproj \
-scheme NightcapPhone \
-destination "platform=iOS Simulator,id=$SIM_UDID" \
-derivedDataPath DerivedData \
-skipMacroValidation \
CODE_SIGNING_ALLOWED=NO
xcrun simctl install "$SIM_UDID" \
DerivedData/Build/Products/Debug-iphonesimulator/Nightcap.app

- name: Run Maestro flows
run: maestro test .maestro/

- name: Upload Maestro artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: maestro-results
path: ~/.maestro/tests
retention-days: 7
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,9 @@ dist/
*.ipa
*.dSYM.zip
*.dSYM

## local-only spec override
project.local.yml

## SwiftPM local build artifacts
Packages/*/.build/
12 changes: 12 additions & 0 deletions .maestro/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Maestro flows

End-to-end flows for the iPhone companion app.

```bash
# Build and install first (Xcode, scheme NightcapPhone, an iOS simulator),
# then:
maestro test .maestro/
```

Flows run against the stubbed `MacStateTransportClient`, so they exercise the
real UI and reducer without needing a Mac on the other end.
27 changes: 27 additions & 0 deletions .maestro/companion-status.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Verifies the companion's core promise end to end: it shows what the Mac is
# doing, and pausing an app from the phone releases the Mac.
appId: com.abdocodes.nightcap.phone
---
- launchApp:
clearState: true

# The Mac is being kept awake by Ghostty
- assertVisible: "Nightcap"
- assertVisible: "Keeping Mac Awake. 1 app active"
- assertVisible: "Watched Apps"
- assertVisible: "Ghostty"
- assertVisible: "Xcode"

# A paused app is labelled as such rather than silently shown as off
- assertVisible: "zoom.us"
- assertVisible: "Paused"

# Pausing Ghostty from the phone lets the Mac sleep
- tapOn:
id: "Watch Ghostty"
- assertVisible: "Idle. Sleep allowed"

# Resuming brings it back
- tapOn:
id: "Watch Ghostty"
- assertVisible: "Ghostty"
8 changes: 8 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"xcode": {
"command": "xcrun",
"args": ["mcpbridge"]
}
}
}
17 changes: 17 additions & 0 deletions Apps/Phone/NightcapPhoneApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import ComposableArchitecture
import NightcapCompanionUI
import NightcapDomain
import SwiftUI

@main
struct NightcapPhoneApp: App {
@State private var store = Store(initialState: CompanionFeature.State()) {
CompanionFeature()
}

var body: some Scene {
WindowGroup {
CompanionView(store: store)
}
}
}
17 changes: 17 additions & 0 deletions Apps/Watch/NightcapWatchApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import ComposableArchitecture
import NightcapCompanionUI
import NightcapDomain
import SwiftUI

@main
struct NightcapWatchApp: App {
@State private var store = Store(initialState: CompanionFeature.State()) {
CompanionFeature()
}

var body: some Scene {
WindowGroup {
CompanionView(store: store)
}
}
}
3 changes: 3 additions & 0 deletions Nightcap/NightcapApp.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import ComposableArchitecture
import NightcapClients
import NightcapDomain
import NightcapUI
import SwiftUI

@main
Expand Down
27 changes: 0 additions & 27 deletions Nightcap/Services/AppQuitterClient.swift

This file was deleted.

29 changes: 0 additions & 29 deletions Nightcap/Services/LaunchAtLoginClient.swift

This file was deleted.

Loading