Skip to content
Merged
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
6 changes: 4 additions & 2 deletions MonitorControl.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
F0A489C4279C71B200BEDFD6 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A489C3279C71B200BEDFD6 /* OnboardingViewController.swift */; };
FE4E0896249D584C003A50BB /* OSDUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE4E0895249D584C003A50BB /* OSDUtils.swift */; };
E77C0DE000000000000000A3 /* XDRBrightnessTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E77C0DE000000000000000A1 /* XDRBrightnessTests.swift */; };
E77C0DE000000000000000B3 /* SettingsPanesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E77C0DE000000000000000B1 /* SettingsPanesTests.swift */; };
/* End PBXBuildFile section */

/* Begin PBXCopyFilesBuildPhase section */
Expand Down Expand Up @@ -185,6 +186,7 @@
FB5DB2902AD54C4600306223 /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = "<group>"; };
FE4E0895249D584C003A50BB /* OSDUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSDUtils.swift; sourceTree = "<group>"; };
E77C0DE000000000000000A1 /* XDRBrightnessTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XDRBrightnessTests.swift; sourceTree = "<group>"; };
E77C0DE000000000000000B1 /* SettingsPanesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPanesTests.swift; sourceTree = "<group>"; };
E77C0DE000000000000000A2 /* MonitorControlTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; name = MonitorControlTests.xctest; path = MonitorControlTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -390,6 +392,7 @@
isa = PBXGroup;
children = (
E77C0DE000000000000000A1 /* XDRBrightnessTests.swift */,
E77C0DE000000000000000B1 /* SettingsPanesTests.swift */,
);
path = MonitorControlTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -745,6 +748,7 @@
buildActionMask = 2147483647;
files = (
E77C0DE000000000000000A3 /* XDRBrightnessTests.swift in Sources */,
E77C0DE000000000000000B3 /* SettingsPanesTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -996,7 +1000,6 @@
PRODUCT_NAME = LumaControl;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "MonitorControl/Support/Bridging-Header.h";
SWIFT_MODULE_NAME = XDRMonitorControl;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.5;
SYSTEM_FRAMEWORK_SEARCH_PATHS = (
Expand Down Expand Up @@ -1035,7 +1038,6 @@
PRODUCT_NAME = LumaControl;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "MonitorControl/Support/Bridging-Header.h";
SWIFT_MODULE_NAME = XDRMonitorControl;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.5;
SYSTEM_FRAMEWORK_SEARCH_PATHS = (
Expand Down
75 changes: 75 additions & 0 deletions MonitorControlTests/SettingsPanesTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright © MonitorControl. @JoniVR, @theOneyouseek, @waydabber and others

import Cocoa
import XCTest

@testable import LumaControl

// The Settings window is assembled at launch from five storyboard scenes, and when that
// goes wrong it goes wrong silently.
//
// `NSStoryboard.instantiateController(withIdentifier:)` does not raise when a scene's
// `customModule` names a Swift module that does not exist at runtime. AppKit only logs
// "Unknown class … in Interface Builder file" to the console, hands back a plain
// `NSViewController`, and the `as?` cast in `main.swift` yields nil. `AppDelegate`
// then drops every pane, so the gear button and reopening the app both show
// "Settings could not be opened" with nothing on screen to say why.
//
// That is exactly what happened when the app target was renamed to LumaControl. Every
// scene is declared `customModuleProvider="target"`, so ibtool bakes `PRODUCT_MODULE_NAME`
// into the compiled storyboard — and `PRODUCT_MODULE_NAME` follows `PRODUCT_NAME`, which
// had become `LumaControl`. The app target still carried an explicit
// `SWIFT_MODULE_NAME = XDRMonitorControl`, and that is the string the Swift compiler uses
// for `-module-name` and therefore for the runtime class names. The storyboard asked for
// `LumaControl.MainPrefsViewController`, the binary only had
// `XDRMonitorControl.MainPrefsViewController`, and all five panes vanished.
//
// These tests load the same scenes the same way `main.swift` does, so they fail loudly the
// moment the two names drift apart again. They need no display and touch no hardware.
final class SettingsPanesTests: XCTestCase {
func testEverySettingsPaneLoadsFromTheStoryboard() {
let storyboard = NSStoryboard(name: "Main", bundle: Bundle.main)

let main = storyboard.instantiateController(withIdentifier: "MainPrefsVC") as? MainPrefsViewController
XCTAssertNotNil(main, "The MainPrefsVC scene did not load — see the note at the top of SettingsPanesTests.")

let menusliders = storyboard.instantiateController(withIdentifier: "MenuslidersPrefsVC") as? MenuslidersPrefsViewController
XCTAssertNotNil(menusliders, "The MenuslidersPrefsVC scene did not load — see the note at the top of SettingsPanesTests.")

let keyboard = storyboard.instantiateController(withIdentifier: "KeyboardPrefsVC") as? KeyboardPrefsViewController
XCTAssertNotNil(keyboard, "The KeyboardPrefsVC scene did not load — see the note at the top of SettingsPanesTests.")

let displays = storyboard.instantiateController(withIdentifier: "DisplaysPrefsVC") as? DisplaysPrefsViewController
XCTAssertNotNil(displays, "The DisplaysPrefsVC scene did not load — see the note at the top of SettingsPanesTests.")

let about = storyboard.instantiateController(withIdentifier: "AboutPrefsVC") as? AboutPrefsViewController
XCTAssertNotNil(about, "The AboutPrefsVC scene did not load — see the note at the top of SettingsPanesTests.")
}

/// The storyboard resolves its scenes against `PRODUCT_MODULE_NAME`, and for an app
/// target that is `PRODUCT_NAME` — the same value `CFBundleName` carries. So the runtime
/// name of a pane class has to start with the app's own name. Comparing against
/// `CFBundleName` rather than a literal means a future rename updates this check for
/// free while still catching the case where the Swift module is pinned to an old name.
func testPaneClassesLiveInTheModuleTheStoryboardResolvesAgainst() {
let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String
XCTAssertEqual(appName, "LumaControl", "CFBundleName should follow PRODUCT_NAME.")

let paneClasses: [AnyClass] = [
MainPrefsViewController.self,
MenuslidersPrefsViewController.self,
KeyboardPrefsViewController.self,
DisplaysPrefsViewController.self,
AboutPrefsViewController.self,
]

for paneClass in paneClasses {
let runtimeName = NSStringFromClass(paneClass)
XCTAssertEqual(
runtimeName.split(separator: ".").first.map(String.init),
appName,
"\(runtimeName) is compiled into a different module than the storyboard resolves against."
)
}
}
}
2 changes: 1 addition & 1 deletion MonitorControlTests/XDRBrightnessTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import XCTest

@testable import XDRMonitorControl
@testable import LumaControl

// AppleDisplay.getBrightness() short-circuits to a constant for dummy displays, so
// calcNewBrightness tests use this stub to drive the current brightness directly.
Expand Down
Loading