Add mock update feed UI tests
This commit is contained in:
parent
ca9c680da7
commit
03ee628fb6
5 changed files with 120 additions and 0 deletions
45
.github/workflows/ci.yml
vendored
Normal file
45
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
ui-tests:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Select Xcode
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -d "/Applications/Xcode.app/Contents/Developer" ]; then
|
||||
XCODE_DIR="/Applications/Xcode.app/Contents/Developer"
|
||||
else
|
||||
XCODE_APP="$(ls -d /Applications/Xcode*.app 2>/dev/null | head -n 1 || true)"
|
||||
if [ -n "$XCODE_APP" ]; then
|
||||
XCODE_DIR="$XCODE_APP/Contents/Developer"
|
||||
else
|
||||
echo "No Xcode.app found under /Applications" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo "DEVELOPER_DIR=$XCODE_DIR" >> "$GITHUB_ENV"
|
||||
export DEVELOPER_DIR="$XCODE_DIR"
|
||||
xcodebuild -version
|
||||
xcrun --sdk macosx --show-sdk-path
|
||||
|
||||
- name: Run UI tests
|
||||
run: |
|
||||
xcodebuild \
|
||||
-project GhosttyTabs.xcodeproj \
|
||||
-scheme cmux \
|
||||
-configuration Debug \
|
||||
-destination 'platform=macOS' \
|
||||
-only-testing:GhosttyTabsUITests/UpdatePillUITests \
|
||||
test
|
||||
|
|
@ -26,6 +26,14 @@ xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug -des
|
|||
./scripts/reload2.sh
|
||||
```
|
||||
|
||||
## E2E mac UI tests
|
||||
|
||||
Run UI tests on the UTM macOS VM (never on the host machine):
|
||||
|
||||
```bash
|
||||
ssh cmux-vm 'cd /Users/cmux/GhosttyTabs && xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug -destination "platform=macOS" -only-testing:GhosttyTabsUITests/UpdatePillUITests test'
|
||||
```
|
||||
|
||||
## Release
|
||||
|
||||
Tagging a version triggers the GitHub Actions release workflow and uploads the notarized zip.
|
||||
|
|
|
|||
|
|
@ -58,6 +58,45 @@ final class UpdatePillUITests: XCTestCase {
|
|||
XCTAssertGreaterThanOrEqual(hiddenAt - shownAt, 4.8)
|
||||
}
|
||||
|
||||
func testCheckForUpdatesUsesMockFeedWithUpdate() {
|
||||
let systemSettings = XCUIApplication(bundleIdentifier: "com.apple.systempreferences")
|
||||
systemSettings.terminate()
|
||||
let app = launchAppWithMockFeed(mode: "available", version: "9.9.9")
|
||||
|
||||
let pill = app.descendants(matching: .any)["UpdatePill"]
|
||||
XCTAssertTrue(pill.waitForExistence(timeout: 5.0))
|
||||
XCTAssertTrue(waitForLabel(pill, label: "Update Available: 9.9.9", timeout: 5.0))
|
||||
assertVisibleSize(pill)
|
||||
attachScreenshot(name: "mock-update-available")
|
||||
}
|
||||
|
||||
func testCheckForUpdatesUsesMockFeedWithNoUpdate() {
|
||||
let systemSettings = XCUIApplication(bundleIdentifier: "com.apple.systempreferences")
|
||||
systemSettings.terminate()
|
||||
let timingPath = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("cmux-ui-test-timing-\(UUID().uuidString).json")
|
||||
let app = launchAppWithMockFeed(mode: "none", version: "9.9.9", timingPath: timingPath)
|
||||
|
||||
let pill = app.descendants(matching: .any)["UpdatePill"]
|
||||
XCTAssertTrue(pill.waitForExistence(timeout: 5.0))
|
||||
XCTAssertTrue(waitForLabel(pill, label: "No Updates Available", timeout: 5.0))
|
||||
assertVisibleSize(pill)
|
||||
attachScreenshot(name: "mock-no-updates")
|
||||
|
||||
let gone = XCTNSPredicateExpectation(
|
||||
predicate: NSPredicate(format: "exists == false"),
|
||||
object: pill
|
||||
)
|
||||
XCTAssertEqual(XCTWaiter().wait(for: [gone], timeout: 7.0), .completed)
|
||||
|
||||
let payload = loadTimingPayload(from: timingPath)
|
||||
let shownAt = payload["noUpdateShownAt"] ?? 0
|
||||
let hiddenAt = payload["noUpdateHiddenAt"] ?? 0
|
||||
XCTAssertGreaterThan(shownAt, 0)
|
||||
XCTAssertGreaterThan(hiddenAt, shownAt)
|
||||
XCTAssertGreaterThanOrEqual(hiddenAt - shownAt, 4.8)
|
||||
}
|
||||
|
||||
private func waitForLabel(_ element: XCUIElement, label: String, timeout: TimeInterval) -> Bool {
|
||||
let predicate = NSPredicate(format: "label == %@", label)
|
||||
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element)
|
||||
|
|
@ -77,6 +116,21 @@ final class UpdatePillUITests: XCTestCase {
|
|||
add(attachment)
|
||||
}
|
||||
|
||||
private func launchAppWithMockFeed(mode: String, version: String, timingPath: URL? = nil) -> XCUIApplication {
|
||||
let app = XCUIApplication()
|
||||
app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1"
|
||||
app.launchEnvironment["CMUX_UI_TEST_FEED_URL"] = "https://cmuxterm.test/appcast.xml"
|
||||
app.launchEnvironment["CMUX_UI_TEST_FEED_MODE"] = mode
|
||||
app.launchEnvironment["CMUX_UI_TEST_UPDATE_VERSION"] = version
|
||||
app.launchEnvironment["CMUX_UI_TEST_TRIGGER_UPDATE_CHECK"] = "1"
|
||||
if let timingPath {
|
||||
app.launchEnvironment["CMUX_UI_TEST_TIMING_PATH"] = timingPath.path
|
||||
}
|
||||
app.launch()
|
||||
app.activate()
|
||||
return app
|
||||
}
|
||||
|
||||
private func loadTimingPayload(from url: URL) -> [String: Double] {
|
||||
guard let data = try? Data(contentsOf: url),
|
||||
let object = try? JSONSerialization.jsonObject(with: data) as? [String: Double] else {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
titlebarAccessoryController.start()
|
||||
#if DEBUG
|
||||
UpdateTestSupport.applyIfNeeded(to: updateController.viewModel)
|
||||
if ProcessInfo.processInfo.environment["CMUX_UI_TEST_TRIGGER_UPDATE_CHECK"] == "1" {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in
|
||||
self?.updateController.checkForUpdates()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,14 @@ import Cocoa
|
|||
|
||||
extension UpdateDriver: SPUUpdaterDelegate {
|
||||
func feedURLString(for updater: SPUUpdater) -> String? {
|
||||
#if DEBUG
|
||||
let env = ProcessInfo.processInfo.environment
|
||||
if let override = env["CMUX_UI_TEST_FEED_URL"], !override.isEmpty {
|
||||
UpdateTestURLProtocol.registerIfNeeded()
|
||||
recordFeedURLString(override, usedFallback: false)
|
||||
return override
|
||||
}
|
||||
#endif
|
||||
let infoURL = Bundle.main.object(forInfoDictionaryKey: "SUFeedURL") as? String
|
||||
let fallback = "https://github.com/manaflow-ai/cmuxterm/releases/latest/download/appcast.xml"
|
||||
let feedURLString = (infoURL?.isEmpty == false) ? infoURL! : fallback
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue