Search code examples
uikit

Why Is Asserting Resolved UIColor Failing After Changing UIUserInterfaceStyle in Unit Test?


I am working on a project that uses named color assets, and I am responsible for updating the unit tests that assert the correct color values.

We have an XCTestCase subclass (BaseXCTestCase) that has a window property, which is set to a value of UIApplication.shared.firstKeyWindow:

var window = UIApplication.shared.firstKeyWindow!

firstKeyWindow is defined as windows.filter { $0.isKeyWindow }.first in an UIApplication extension:

var firstKeyWindow: UIWindow? {
    return windows.filter { $0.isKeyWindow }.first
}

Additionally, there are two properties used for accessing window's traitCollection and userInterfaceStyle:

var traitCollection: UITraitCollection {
    return window.traitCollection
}

var userInterfaceStyle: UIUserInterfaceStyle {
    get {
        return window.traitCollection.userInterfaceStyle
    }
    set {
        window.overrideUserInterfaceStyle = newValue
    }
}

The class that contains the color tests inherits from BaseXCTestCase. Then, our tests are configured as:

func testColor() {
  var subject = .namedColor

  /// Light mode
  userInterfaceStyle = .light
  subject = subject.resolvedColor(with: traitCollection)
  XCTAssertEqual(subject.hexString(), "#ABC123")
  XCTAssertEqual(subject.alpha, 1.0)

  /// Dark mode
  userInterfaceStyle = .dark
  subject = subject.resolvedColor(with: traitCollection)
  XCTAssertEqual(subject.hexString(), "#BCD234")
  XCTAssertEqual(subject.alpha, 1.0)

}

The first two assertions of the test succeed when asserting the color for light mode. However, the assertions for the dark mode color fail, and the failure messages indicate that the light mode color and alpha values are persisting.

I set a breakpoint at the final light-mode assertion, and printing traitCollection provides me with the following output, which I expect:

<UITraitCollection: 0x60000198a760; 
  UserInterfaceIdiom = Phone, 
  DisplayScale = 3, 
  DisplayGamut = P3, 
  HorizontalSizeClass = Compact, 
  VerticalSizeClass = Regular,
  UserInterfaceStyle = Light, 
  UserInterfaceLayoutDirection = LTR, 
  ForceTouchCapability = Unavailable,
  PreferredContentSizeCategory = L, 
  AccessibilityContrast = Normal, 
  UserInterfaceLevel = Base
>

In the printed output, I see UserInterfaceStyle = Light.

Then, I set a breakpoint at the final dark-mode assertion, and printing traitCollection provides me with the following output, which I expect:

<UITraitCollection: 0x600001990460; 
  UserInterfaceIdiom = Phone, 
  DisplayScale = 3, 
  DisplayGamut = P3, 
  HorizontalSizeClass = Compact, 
  VerticalSizeClass = Regular, 
  UserInterfaceStyle = Dark, 
  UserInterfaceLayoutDirection = LTR, 
  ForceTouchCapability = Unavailable, 
  PreferredContentSizeCategory = L, 
  AccessibilityContrast = Normal, 
  UserInterfaceLevel = Base
>

This time, I see UserInterfaceStyle = Dark, which I expect.

If I remove the code that sets the interface style to light and sets the resolved color, then the test passes:

func testColor() {
  var subject = .namedColor

  /// Light mode
  XCTAssertEqual(subject.hexString(), "#ABC123")
  XCTAssertEqual(subject.alpha, 1.0)

  /// Dark mode
  userInterfaceStyle = .dark
  subject = subject.resolvedColor(with: traitCollection)
  XCTAssertEqual(subject.hexString(), "#BCD234")
  XCTAssertEqual(subject.alpha, 1.0)

}

My question is then, why are the assertions for the dark-mode color and alpha values failing? Specifically, what is causing the color to not be resolved after overriding window's overrideUserInterfaceStyle?

Shouldn't the following code cause the color to be resolved with the trait collection that contains UserInterfaceStyle = Dark as the dark-mode color and alpha values?

userInterfaceStyle = .dark
subject = subject.resolvedColor(with: traitCollection)

If I move the dark-mode logic above the light-mode logic, then the light-mode assertions fail. I don't know what I am failing to do between the first assertions and second assertions to ensure the user interface style is being respected.


Solution

  • I don't think unit tests are guaranteed to run on the main thread, so maybe try a DispatchQueue.main.async for the UI update and see how that goes.