diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 821e954..8154089 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,14 +10,20 @@ env: SCHEME: "Codeagogo" DESTINATION: "platform=macOS" +# Explicit bash runs steps with `-eo pipefail`, so an xcodebuild failure is not +# masked by the exit status of the xcbeautify formatter it is piped into. +defaults: + run: + shell: bash + jobs: lint: name: SwiftLint - runs-on: macos-14 + runs-on: macos-26 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Install SwiftLint run: brew install swiftlint @@ -27,22 +33,22 @@ jobs: audit: name: Dependency Audit - runs-on: macos-14 + runs-on: macos-26 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Audit Dependencies run: ./scripts/audit-dependencies.sh secrets: name: Secrets Scan - runs-on: macos-14 + runs-on: macos-26 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 @@ -54,26 +60,26 @@ jobs: license-headers: name: License Headers - runs-on: macos-14 + runs-on: macos-26 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Check license headers run: ./scripts/license-header.sh --check build-and-test: name: Build and Test - runs-on: macos-14 + runs-on: macos-26 needs: [lint, audit, secrets, license-headers] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Select Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_26.6.app/Contents/Developer - name: Show Xcode version run: xcodebuild -version @@ -88,46 +94,50 @@ jobs: CODE_SIGNING_REQUIRED=NO \ ENABLE_HARDENED_RUNTIME=NO \ ENABLE_APP_SANDBOX=NO \ - | xcpretty + | xcbeautify - name: Unit Tests run: | xcodebuild test \ -scheme "$SCHEME" \ -destination "$DESTINATION" \ - -only-testing:"CodeagogoTests" \ + -testPlan "Codeagogo-Unit" \ -configuration Debug \ CODE_SIGN_IDENTITY="-" \ CODE_SIGNING_REQUIRED=NO \ ENABLE_HARDENED_RUNTIME=NO \ ENABLE_APP_SANDBOX=NO \ - | xcpretty + | xcbeautify + # The UI tests live only in the Codeagogo-UI test plan (it also skips the + # SearchPanelUITests suite, which hangs XCUITest). GitHub's macOS runners + # have a logged-in GUI session, which XCUITest needs; no display required. - name: UI Tests + timeout-minutes: 20 run: | xcodebuild test \ -scheme "$SCHEME" \ -destination "$DESTINATION" \ - -only-testing:"CodeagogoUITests" \ + -testPlan "Codeagogo-UI" \ -configuration Debug \ CODE_SIGN_IDENTITY="-" \ CODE_SIGNING_REQUIRED=YES \ ENABLE_HARDENED_RUNTIME=NO \ ENABLE_APP_SANDBOX=NO \ - | xcpretty + | xcbeautify build-artifact: name: Build Release Artifact - runs-on: macos-14 + runs-on: macos-26 needs: build-and-test if: github.event_name == 'push' && github.ref == 'refs/heads/main' steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Select Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_26.6.app/Contents/Developer - name: Install Apple Certificate env: @@ -180,7 +190,7 @@ jobs: zip -r "Codeagogo-macOS.zip" "Codeagogo.app" - name: Upload Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: Codeagogo-macOS path: artifact/Codeagogo-macOS.zip diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7912402..3271f96 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ env: jobs: release: name: Create Release - runs-on: macos-14 + runs-on: macos-26 permissions: contents: write @@ -28,7 +28,7 @@ jobs: fi - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} @@ -107,7 +107,7 @@ jobs: git push origin "v${{ inputs.version }}" - name: Select Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_26.6.app/Contents/Developer - name: Install Apple Certificate if: env.APPLE_CERTIFICATE_BASE64 != '' @@ -174,7 +174,7 @@ jobs: zip -r "Codeagogo-v${{ inputs.version }}-macOS.zip" "Codeagogo.app" - name: Create GitHub Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: tag_name: v${{ inputs.version }} name: Codeagogo v${{ inputs.version }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ebc5d9..cb7853b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Fixed +- **Settings window on smaller screens**: The Settings window had a fixed 1060pt height, so on shorter displays (such as a 13" MacBook Air) its bottom extended off-screen and the logging and diagnostics controls could not be reached. It now opens at most as tall as the screen's visible area, scrolls when the content doesn't fit, and can be resized between 480pt and its full height. + ## [1.1.1] - 2026-06 ### Fixed diff --git a/Codeagogo/CodeagogoApp.swift b/Codeagogo/CodeagogoApp.swift index 76ab05e..e3ce3f7 100644 --- a/Codeagogo/CodeagogoApp.swift +++ b/Codeagogo/CodeagogoApp.swift @@ -22,6 +22,8 @@ struct Snomed_LookupApp: App { Settings { SettingsView() } + // Size the window from SettingsView's min/ideal/max height so it fits the screen. + .windowResizability(.contentSize) .commands { // Add items in the Help menu (native place for diagnostics) CommandGroup(after: .help) { diff --git a/Codeagogo/SettingsView.swift b/Codeagogo/SettingsView.swift index 404c2ad..623f25b 100644 --- a/Codeagogo/SettingsView.swift +++ b/Codeagogo/SettingsView.swift @@ -414,7 +414,27 @@ struct SettingsView: View { } .padding(16) } - .frame(width: 520, height: 1060) + .frame(width: 520) + .frame(minHeight: Self.minimumHeight, idealHeight: Self.fittingHeight, maxHeight: Self.contentHeight) + } + + // MARK: - Window Sizing + + /// Height at which all settings are visible without scrolling. + private static let contentHeight: CGFloat = 1060 + + /// Smallest height the window can be resized to. + private static let minimumHeight: CGFloat = 480 + + /// Space reserved for the window's title bar when fitting it to the screen. + private static let titleBarAllowance: CGFloat = 40 + + /// Initial window height: the full content height, capped to the main screen's + /// visible area so the window never extends past the bottom of smaller screens. + /// The scroll view handles the overflow. + private static var fittingHeight: CGFloat { + guard let visibleHeight = NSScreen.main?.visibleFrame.height else { return contentHeight } + return max(minimumHeight, min(contentHeight, visibleHeight - titleBarAllowance)) } // MARK: - Code System Helpers diff --git a/CodeagogoUITests/CodeagogoUITestCase.swift b/CodeagogoUITests/CodeagogoUITestCase.swift index 109b459..6621541 100644 --- a/CodeagogoUITests/CodeagogoUITestCase.swift +++ b/CodeagogoUITests/CodeagogoUITestCase.swift @@ -66,4 +66,21 @@ class SNOMEDLookupUITestCase: XCTestCase { } return settingsWindow } + + /// Scrolls the window's scroll view until `element` can be clicked. + /// + /// The Settings window is taller than small screens (such as CI runners), + /// so controls near the bottom of its scroll view start out off-screen and + /// are not hittable until scrolled into view. + /// + /// - Parameters: + /// - element: The element to bring into view. + /// - window: The window containing the scroll view. + func scrollIntoView(_ element: XCUIElement, in window: XCUIElement) { + let scrollView = window.scrollViews.firstMatch + guard scrollView.exists else { return } + for _ in 0..<10 where !element.isHittable { + scrollView.scroll(byDeltaX: 0, deltaY: -200) + } + } } diff --git a/CodeagogoUITests/SettingsViewUITests.swift b/CodeagogoUITests/SettingsViewUITests.swift index 2846374..3f48020 100644 --- a/CodeagogoUITests/SettingsViewUITests.swift +++ b/CodeagogoUITests/SettingsViewUITests.swift @@ -188,6 +188,7 @@ final class SettingsViewUITests: SNOMEDLookupUITestCase { XCTFail("Debug logging toggle not found") return } + scrollIntoView(toggle, in: settingsWindow) let initialValue = toggle.value as? Int ?? 0 toggle.click() let newValue = toggle.value as? Int ?? 0 @@ -212,6 +213,7 @@ final class SettingsViewUITests: SNOMEDLookupUITestCase { return } XCTAssertTrue(button.isEnabled, "Diagnostics button should be enabled") + scrollIntoView(button, in: settingsWindow) button.click() // Button should still exist after clicking XCTAssertTrue(button.exists,