Skip to content

Commit

Permalink
Update M3CTextView to call applyAllColor when setting a color for a…
Browse files Browse the repository at this point in the history
… state, and add unit tests.

PiperOrigin-RevId: 685772722
  • Loading branch information
CGRect authored and material-automation committed Oct 14, 2024
1 parent 55edb9a commit 6046d06
Show file tree
Hide file tree
Showing 2 changed files with 333 additions and 7 deletions.
35 changes: 28 additions & 7 deletions components/M3CTextField/src/M3CTextView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ public final class M3CTextView: UIView, M3CTextInput {
}
}

private var backgroundColors: [UIControl.State: UIColor] = [:]
private var borderColors: [UIControl.State: UIColor] = [:]
private var inputColors: [UIControl.State: UIColor] = [:]
private var supportingLabelColors: [UIControl.State: UIColor] = [:]
private var titleLabelColors: [UIControl.State: UIColor] = [:]
private var trailingLabelColors: [UIControl.State: UIColor] = [:]
private var tintColors: [UIControl.State: UIColor] = [:]
var backgroundColors: [UIControl.State: UIColor] = [:]
var borderColors: [UIControl.State: UIColor] = [:]
var inputColors: [UIControl.State: UIColor] = [:]
var supportingLabelColors: [UIControl.State: UIColor] = [:]
var titleLabelColors: [UIControl.State: UIColor] = [:]
var trailingLabelColors: [UIControl.State: UIColor] = [:]
var tintColors: [UIControl.State: UIColor] = [:]

@objc public lazy var textContainer: UITextView = {
let textContainer = M3CSelectableTextView()
Expand Down Expand Up @@ -91,42 +91,63 @@ public final class M3CTextView: UIView, M3CTextInput {
@objc(setBackgroundColor:forState:)
public func setBackgroundColor(_ color: UIColor?, for state: UIControl.State) {
backgroundColors[state] = color
if state == controlState {
applyAllColors()
}
}

/// Sets the border color for a specific UIControlState.
@objc(setBorderColor:forState:)
public func setBorderColor(_ color: UIColor?, for state: UIControl.State) {
borderColors[state] = color
if state == controlState {
applyAllColors()
}
}

/// Sets the input color for a specific UIControlState.
@objc(setInputColor:forState:)
public func setInputColor(_ color: UIColor?, for state: UIControl.State) {
inputColors[state] = color
if state == controlState {
applyAllColors()
}
}

/// Sets the supporting label color for a specific UIControlState.
@objc(setSupportingLabelColor:forState:)
public func setSupportingLabelColor(_ color: UIColor?, for state: UIControl.State) {
supportingLabelColors[state] = color
if state == controlState {
applyAllColors()
}
}

/// Sets the tint color for a specific UIControlState.
@objc(setTintColor:forState:)
public func setTintColor(_ color: UIColor?, for state: UIControl.State) {
tintColors[state] = color
if state == controlState {
applyAllColors()
}
}

/// Sets the title label color for a specific UIControlState.
@objc(setTitleLabelColor:forState:)
public func setTitleLabelColor(_ color: UIColor?, for state: UIControl.State) {
titleLabelColors[state] = color
if state == controlState {
applyAllColors()
}
}

/// Sets the trailing label color for a specific UIControlState.
@objc(setTrailingLabelColor:forState:)
public func setTrailingLabelColor(_ color: UIColor?, for state: UIControl.State) {
trailingLabelColors[state] = color
if state == controlState {
applyAllColors()
}
}

override public func layoutSubviews() {
Expand Down
305 changes: 305 additions & 0 deletions components/M3CTextField/tests/unit/M3CTextViewTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
// Copyright 2019-present the Material Components for iOS authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import XCTest

@testable import MaterialComponents.MaterialM3CTextField

class M3CTextViewTests: XCTestCase {
let testColorValues: [UIControl.State: UIColor] = [
.normal: .black, .error: .red, .selected: .blue,
]

var sutTextView: M3CTextView!
var sutTextContainer: UITextView!

override func setUp() {
super.setUp()
sutTextView = M3CTextView()
sutTextContainer = sutTextView.textContainer
}

override func tearDown() {
sutTextView = nil
super.tearDown()
}

/// Tests that the expected colors are applied for the .normal to .error case.
/// This includes verifying:
/// - Expected colors for background, border, labels, input text, and tint.
/// - !isFirstResponder unchanged.
func testApplyColorsFromNormalToErrorState() {
applyInitialColorTestingConfiguration(for: .normal)
let normalColor = expectedColor(for: .normal)
sutTextContainer.resignFirstResponder()
assertSUTExpectations(for: normalColor)
XCTAssertFalse(sutTextContainer.isFirstResponder)

sutTextView.isInErrorState = true

let errorColor = expectedColor(for: .error)
assertSUTExpectations(for: errorColor)
XCTAssertFalse(sutTextContainer.isFirstResponder)
}

/// Tests that the expected colors are applied for the .normal to .selected case.
/// This includes verifying:
/// - Expected colors for background, border, labels, input text, and tint.
/// - !isFirstResponder toggled to isFirstResponder.
func testApplyColorsFromNormalToSelectedState() {
applyInitialColorTestingConfiguration(for: .normal)
let normalColor = expectedColor(for: .normal)
sutTextContainer.resignFirstResponder()
assertSUTExpectations(for: normalColor)
XCTAssertFalse(sutTextContainer.isFirstResponder)

sutTextView.isInErrorState = false
sutTextContainer.becomeFirstResponder()

let selectedColor = expectedColor(for: .selected)
assertSUTExpectations(for: selectedColor)
XCTAssertTrue(sutTextContainer.isFirstResponder)
}

/// Tests that the expected colors are applied for the .error to .normal case.
/// This includes verifying:
/// - Expected colors for background, border, labels, input text, and tint.
/// - !isFirstResponder unchanged.
func testApplyColorsFromErrorToNormalState() {
applyInitialColorTestingConfiguration(for: .error)
sutTextView.isInErrorState = true
let errorColor = expectedColor(for: .error)
sutTextContainer.resignFirstResponder()
assertSUTExpectations(for: errorColor)
XCTAssertFalse(sutTextContainer.isFirstResponder)

sutTextView.isInErrorState = false

let normalColor = expectedColor(for: .normal)
assertSUTExpectations(for: normalColor)
XCTAssertFalse(sutTextContainer.isFirstResponder)
}

/// Tests that the expected colors are applied for the .error to .selected case.
/// This includes verifying:
/// - Expected colors for background, border, labels, input text, and tint.
/// - !isFirstResponder toggled to isFirstResponder.
func testApplyColorsFromErrorToSelectedState() {
applyInitialColorTestingConfiguration(for: .error)
sutTextView.isInErrorState = true
let errorColor = expectedColor(for: .error)
sutTextContainer.resignFirstResponder()
assertSUTExpectations(for: errorColor)
XCTAssertFalse(sutTextContainer.isFirstResponder)

sutTextView.isInErrorState = false
sutTextContainer.becomeFirstResponder()

let selectedColor = expectedColor(for: .selected)
assertSUTExpectations(for: selectedColor)
XCTAssertTrue(sutTextContainer.isFirstResponder)
}

/// Tests that the expected colors are applied for the .selected to .normal case.
/// This includes verifying:
/// - Expected colors for background, border, labels, input text, and tint.
/// - isFirstResponder toggled to !isFirstResponder.
func testApplyColorsFromSelectedToNormalState() {
applyInitialColorTestingConfiguration(for: .selected)
let selectedColor = expectedColor(for: .selected)
let sutTextContainer = sutTextView.textContainer
sutTextContainer.becomeFirstResponder()
assertSUTExpectations(for: selectedColor)
XCTAssertTrue(sutTextContainer.isFirstResponder)

sutTextContainer.resignFirstResponder()

let normalColor = expectedColor(for: .normal)
assertSUTExpectations(for: normalColor)
XCTAssertFalse(sutTextContainer.isFirstResponder)
}

/// Tests that the expected colors are applied for the .selected to .error case.
/// This includes verifying:
/// - Expected colors for background, border, labels, input text, and tint.
/// - isFirstResponder unchanged.
func testApplyColorsFromSelectedToErrorState() {
applyInitialColorTestingConfiguration(for: .selected)
let selectedColor = expectedColor(for: .selected)
sutTextContainer.becomeFirstResponder()
assertSUTExpectations(for: selectedColor)
XCTAssertTrue(sutTextContainer.isFirstResponder)

sutTextView.isInErrorState = true

let errorColor = expectedColor(for: .error)
assertSUTExpectations(for: errorColor)
XCTAssertTrue(sutTextContainer.isFirstResponder)
}

/// Tests that a new background color for a specific state is applied to the underlying
/// UITextView when calling `setBackgroundColor`.
func testSetBackgroundColorAppliesNewColor() {
applyInitialColorTestingConfiguration(for: .normal)
let defaultColor = expectedColor(for: .normal)
let customColor = UIColor.green
XCTAssertEqual(sutTextContainer.backgroundColor, defaultColor)

sutTextView.setBackgroundColor(customColor, for: UIControl.State.normal)

XCTAssertEqual(sutTextContainer.backgroundColor, customColor)
}

/// Tests that a new border color for a specific state is applied to the underlying
/// UITextView when calling `setBorderColor`.
func testSetBorderColorAppliesNewColor() {
applyInitialColorTestingConfiguration(for: .normal)
let defaultColor = expectedColor(for: .normal)
let customColor = UIColor.green
XCTAssertEqual(sutTextContainer.layer.borderColor, defaultColor.cgColor)

sutTextView.setBorderColor(customColor, for: UIControl.State.normal)

XCTAssertEqual(sutTextContainer.layer.borderColor, customColor.cgColor)
}

/// Tests that a new input color for a specific state is applied to the underlying
/// UITextView when calling `setInputColor`.
func testSetInputColorAppliesNewColor() {
applyInitialColorTestingConfiguration(for: .normal)
let defaultColor = expectedColor(for: .normal)
let customColor = UIColor.green
XCTAssertEqual(sutTextContainer.textColor, defaultColor)

sutTextView.setInputColor(customColor, for: UIControl.State.normal)

XCTAssertEqual(sutTextContainer.textColor, customColor)
}

/// Tests that a new supporting label color for a specific state is applied to the underlying
/// UILabel when calling `setSupportingLabelColor`.
func testSetSupportingLabelColorAppliesNewColor() {
applyInitialColorTestingConfiguration(for: .normal)
let defaultColor = expectedColor(for: .normal)
let customColor = UIColor.green
XCTAssertEqual(sutTextView.supportingLabel.textColor, defaultColor)

sutTextView.setSupportingLabelColor(customColor, for: UIControl.State.normal)

XCTAssertEqual(sutTextView.supportingLabel.textColor, customColor)
}

/// Tests that a new tint color for a specific state is applied to the underlying
/// UITextView when calling `setTintColor`.
func testSetTintColorAppliesNewColor() {
applyInitialColorTestingConfiguration(for: .normal)
let defaultColor = expectedColor(for: .normal)
let customColor = UIColor.green
XCTAssertEqual(sutTextContainer.tintColor, defaultColor)

sutTextView.setTintColor(customColor, for: UIControl.State.normal)

XCTAssertEqual(sutTextContainer.tintColor, customColor)
}

/// Tests that a new title label color for a specific state is applied to the underlying
/// UILabel when calling `setTitleLabelColor`.
func testSetTitleLabelColorAppliesNewColor() {
applyInitialColorTestingConfiguration(for: .normal)
let defaultColor = expectedColor(for: .normal)
let customColor = UIColor.green
XCTAssertEqual(sutTextView.titleLabel.textColor, defaultColor)

sutTextView.setTitleLabelColor(customColor, for: UIControl.State.normal)

XCTAssertEqual(sutTextView.titleLabel.textColor, customColor)
}

/// Tests that a new trailing label color for a specific state is applied to the underlying
/// UILabel when calling `setTrailingLabelColor`.
func testSetTrailingLabelColorAppliesNewColor() {
applyInitialColorTestingConfiguration(for: .normal)
let defaultColor = expectedColor(for: .normal)
let customColor = UIColor.green
XCTAssertEqual(sutTextView.trailingLabel.textColor, defaultColor)

sutTextView.setTrailingLabelColor(customColor, for: UIControl.State.normal)

XCTAssertEqual(sutTextView.trailingLabel.textColor, customColor)
}
}

// MARK: - Test Assertion Helpers

extension M3CTextViewTests {
private func assertSUTExpectations(
for color: UIColor, file: StaticString = #file, line: UInt = #line
) {
XCTAssertEqual(sutTextContainer.backgroundColor, color, file: file, line: line)
XCTAssertEqual(sutTextContainer.layer.borderColor, color.cgColor, file: file, line: line)
XCTAssertEqual(sutTextContainer.textColor, color, file: file, line: line)
XCTAssertEqual(sutTextContainer.tintColor, color, file: file, line: line)
XCTAssertEqual(sutTextView.supportingLabel.textColor, color, file: file, line: line)
XCTAssertEqual(sutTextView.titleLabel.textColor, color, file: file, line: line)
XCTAssertEqual(sutTextView.trailingLabel.textColor, color, file: file, line: line)
}
}

// MARK: - Test Configuration Helpers

extension M3CTextViewTests {
private func applyInitialColorTestingConfiguration(for controlState: UIControl.State) {
// `becomeFirstResponder` and `resignFirstResponder` require a window.
let window = UIWindow()
window.addSubview(sutTextView)

sutTextView.text = "test"
configureColorDictionaries()
configureTextContentColors(for: controlState)
}

private func configureColorDictionaries() {
sutTextView.backgroundColors = testColorValues
sutTextView.borderColors = testColorValues
sutTextView.inputColors = testColorValues
sutTextView.supportingLabelColors = testColorValues
sutTextView.titleLabelColors = testColorValues
sutTextView.trailingLabelColors = testColorValues
sutTextView.tintColors = testColorValues
}

private func configureTextContentColors(for controlState: UIControl.State) {
let color = expectedColor(for: controlState)
sutTextContainer.backgroundColor = color
sutTextContainer.layer.borderColor = color.cgColor
sutTextContainer.textColor = color
sutTextContainer.tintColor = color
sutTextView.supportingLabel.textColor = color
sutTextView.titleLabel.textColor = color
sutTextView.trailingLabel.textColor = color
}

private func expectedColor(for controlState: UIControl.State) -> UIColor {
switch controlState {
case .normal:
.black
case .error:
.red
case .selected:
.blue
default:
.black
}
}
}

0 comments on commit 6046d06

Please sign in to comment.