Swift Testing for Compound (#5110)

This commit is contained in:
Mauro
2026-02-16 14:04:04 +01:00
committed by GitHub
parent 26bfa6ed7c
commit e8a6af1956
7 changed files with 135 additions and 122 deletions
+3
View File
@@ -35,6 +35,9 @@ let package = Package(
], ],
exclude: [ exclude: [
"__Snapshots__" "__Snapshots__"
],
swiftSettings: [
.defaultIsolation(MainActor.self)
] ]
) )
] ]
@@ -9,10 +9,10 @@
@testable import Compound @testable import Compound
import Foundation import Foundation
import SwiftUI import SwiftUI
import XCTest import Testing
@MainActor @Suite
final class DecorativeColorsTests: XCTestCase { struct DecorativeColorsTests {
struct TestCase { struct TestCase {
let input: String let input: String
private let webOutput: Int private let webOutput: Int
@@ -28,7 +28,8 @@ final class DecorativeColorsTests: XCTestCase {
} }
} }
func testAvatarColorHash() { @Test("Avatar color hash matches web implementation")
func avatarColorHash() {
// Match the tests with the web ones for consistency between the two platforms // Match the tests with the web ones for consistency between the two platforms
// https://github.com/element-hq/compound-web/blob/4608dc807c9c904874eac67ff22be3213f4a261d/src/components/Avatar/Avatar.test.tsx#L62 // https://github.com/element-hq/compound-web/blob/4608dc807c9c904874eac67ff22be3213f4a261d/src/components/Avatar/Avatar.test.tsx#L62
let testCases: [TestCase] = [ let testCases: [TestCase] = [
@@ -41,7 +42,7 @@ final class DecorativeColorsTests: XCTestCase {
] ]
for testCase in testCases { for testCase in testCases {
XCTAssertEqual(Color.compound.decorativeColor(for: testCase.input), Color.compound.decorativeColors[testCase.output]) #expect(Color.compound.decorativeColor(for: testCase.input) == Color.compound.decorativeColors[testCase.output])
} }
} }
} }
@@ -8,100 +8,106 @@
@testable import Compound @testable import Compound
import SwiftUI import SwiftUI
import XCTest import Testing
@MainActor @MainActor
final class FontSizeTests: XCTestCase { @Suite
struct FontSizeTests {
/// Test all system text styles to assert mapping between `Font` and `UIFont`. /// Test all system text styles to assert mapping between `Font` and `UIFont`.
func testTextStyle() { @Test("Text style")
func textStyle() {
let caption2FontSize = FontSize.reflecting(.caption2) let caption2FontSize = FontSize.reflecting(.caption2)
XCTAssertEqual(caption2FontSize?.value, 11) #expect(caption2FontSize?.value == 11)
XCTAssertEqual(caption2FontSize?.style, .caption2) #expect(caption2FontSize?.style == .caption2)
let captionFontSize = FontSize.reflecting(.caption) let captionFontSize = FontSize.reflecting(.caption)
XCTAssertEqual(captionFontSize?.value, 12) #expect(captionFontSize?.value == 12)
XCTAssertEqual(captionFontSize?.style, .caption) #expect(captionFontSize?.style == .caption)
let footnoteFontSize = FontSize.reflecting(.footnote) let footnoteFontSize = FontSize.reflecting(.footnote)
XCTAssertEqual(footnoteFontSize?.value, 13) #expect(footnoteFontSize?.value == 13)
XCTAssertEqual(footnoteFontSize?.style, .footnote) #expect(footnoteFontSize?.style == .footnote)
let subheadlineFontSize = FontSize.reflecting(.subheadline) let subheadlineFontSize = FontSize.reflecting(.subheadline)
XCTAssertEqual(subheadlineFontSize?.value, 15) #expect(subheadlineFontSize?.value == 15)
XCTAssertEqual(subheadlineFontSize?.style, .subheadline) #expect(subheadlineFontSize?.style == .subheadline)
let calloutFontSize = FontSize.reflecting(.callout) let calloutFontSize = FontSize.reflecting(.callout)
XCTAssertEqual(calloutFontSize?.value, 16) #expect(calloutFontSize?.value == 16)
XCTAssertEqual(calloutFontSize?.style, .callout) #expect(calloutFontSize?.style == .callout)
let bodyFontSize = FontSize.reflecting(.body) let bodyFontSize = FontSize.reflecting(.body)
XCTAssertEqual(bodyFontSize?.value, 17) #expect(bodyFontSize?.value == 17)
XCTAssertEqual(bodyFontSize?.style, .body) #expect(bodyFontSize?.style == .body)
let headlineFontSize = FontSize.reflecting(.headline) let headlineFontSize = FontSize.reflecting(.headline)
XCTAssertEqual(headlineFontSize?.value, 17) #expect(headlineFontSize?.value == 17)
XCTAssertEqual(headlineFontSize?.style, .headline) #expect(headlineFontSize?.style == .headline)
let title3FontSize = FontSize.reflecting(.title3) let title3FontSize = FontSize.reflecting(.title3)
XCTAssertEqual(title3FontSize?.value, 20) #expect(title3FontSize?.value == 20)
XCTAssertEqual(title3FontSize?.style, .title3) #expect(title3FontSize?.style == .title3)
let title2FontSize = FontSize.reflecting(.title2) let title2FontSize = FontSize.reflecting(.title2)
XCTAssertEqual(title2FontSize?.value, 22) #expect(title2FontSize?.value == 22)
XCTAssertEqual(title2FontSize?.style, .title2) #expect(title2FontSize?.style == .title2)
let titleFontSize = FontSize.reflecting(.title) let titleFontSize = FontSize.reflecting(.title)
XCTAssertEqual(titleFontSize?.value, 28) #expect(titleFontSize?.value == 28)
XCTAssertEqual(titleFontSize?.style, .title) #expect(titleFontSize?.style == .title)
let largeTitleFontSize = FontSize.reflecting(.largeTitle) let largeTitleFontSize = FontSize.reflecting(.largeTitle)
XCTAssertEqual(largeTitleFontSize?.value, 34) #expect(largeTitleFontSize?.value == 34)
XCTAssertEqual(largeTitleFontSize?.style, .largeTitle) #expect(largeTitleFontSize?.style == .largeTitle)
} }
func testModifiedTextStyle() { @Test("Modified text style font sizes")
func modifiedTextStyle() {
let boldCaptionFontSize = FontSize.reflecting(.caption.bold()) let boldCaptionFontSize = FontSize.reflecting(.caption.bold())
XCTAssertEqual(boldCaptionFontSize?.value, 12) #expect(boldCaptionFontSize?.value == 12)
XCTAssertEqual(boldCaptionFontSize?.style, .caption) #expect(boldCaptionFontSize?.style == .caption)
let styledTitle = Font.title.width(.compressed).bold().italic().monospaced() let styledTitle = Font.title.width(.compressed).bold().italic().monospaced()
let styledTitleFontSize = FontSize.reflecting(styledTitle) let styledTitleFontSize = FontSize.reflecting(styledTitle)
XCTAssertEqual(styledTitleFontSize?.value, 28) #expect(styledTitleFontSize?.value == 28)
XCTAssertEqual(styledTitleFontSize?.style, .title) #expect(styledTitleFontSize?.style == .title)
} }
func testSystemFont() { @Test("System font sizes")
func systemFont() {
let system21FontSize = FontSize.reflecting(.system(size: 21)) let system21FontSize = FontSize.reflecting(.system(size: 21))
XCTAssertEqual(system21FontSize?.value, 21) #expect(system21FontSize?.value == 21)
let boldSystem29FontSize = FontSize.reflecting(.system(size: 29).bold()) let boldSystem29FontSize = FontSize.reflecting(.system(size: 29).bold())
XCTAssertEqual(boldSystem29FontSize?.value, 29) #expect(boldSystem29FontSize?.value == 29)
let styledSystem33 = Font.system(size: 33).width(.compressed).bold().italic().monospacedDigit() let styledSystem33 = Font.system(size: 33).width(.compressed).bold().italic().monospacedDigit()
let styledSystem33FontSize = FontSize.reflecting(styledSystem33) let styledSystem33FontSize = FontSize.reflecting(styledSystem33)
XCTAssertEqual(styledSystem33FontSize?.value, 33) #expect(styledSystem33FontSize?.value == 33)
} }
func testCustomFont() { @Test("Custom font sizes")
func customFont() {
let custom43FontSize = FontSize.reflecting(.custom("Baskerville", size: 43)) let custom43FontSize = FontSize.reflecting(.custom("Baskerville", size: 43))
XCTAssertEqual(custom43FontSize?.value, 43) #expect(custom43FontSize?.value == 43)
XCTAssertEqual(custom43FontSize?.style, .body) #expect(custom43FontSize?.style == .body)
let styledCustom35 = Font.custom("Baskerville", size: 35).weight(.thin).monospaced().italic() let styledCustom35 = Font.custom("Baskerville", size: 35).weight(.thin).monospaced().italic()
let styledCustom35FontSize = FontSize.reflecting(styledCustom35) let styledCustom35FontSize = FontSize.reflecting(styledCustom35)
XCTAssertEqual(styledCustom35FontSize?.value, 35) #expect(styledCustom35FontSize?.value == 35)
XCTAssertEqual(styledCustom35FontSize?.style, .body) #expect(styledCustom35FontSize?.style == .body)
} }
func testCustomFontWithTextStyle() { @Test("Custom font with text style")
func customFontWithTextStyle() {
let customTitle21FontSize = FontSize.reflecting(.custom("Baskerville", size: 21, relativeTo: .title)) let customTitle21FontSize = FontSize.reflecting(.custom("Baskerville", size: 21, relativeTo: .title))
XCTAssertEqual(customTitle21FontSize?.value, 21) #expect(customTitle21FontSize?.value == 21)
XCTAssertEqual(customTitle21FontSize?.style, .title) #expect(customTitle21FontSize?.style == .title)
let styledCustomFootnote15 = Font.custom("Baskerville", size: 15, relativeTo: .footnote).weight(.thin).monospaced().italic() let styledCustomFootnote15 = Font.custom("Baskerville", size: 15, relativeTo: .footnote).weight(.thin).monospaced().italic()
let styledCustomFootnote15FontSize = FontSize.reflecting(styledCustomFootnote15) let styledCustomFootnote15FontSize = FontSize.reflecting(styledCustomFootnote15)
XCTAssertEqual(styledCustomFootnote15FontSize?.value, 15) #expect(styledCustomFootnote15FontSize?.value == 15)
XCTAssertEqual(styledCustomFootnote15FontSize?.style, .footnote) #expect(styledCustomFootnote15FontSize?.style == .footnote)
} }
} }
@@ -4,116 +4,134 @@
// swiftlint:disable all // swiftlint:disable all
// swiftformat:disable all // swiftformat:disable all
import XCTest import Testing
@testable import Compound @testable import Compound
extension PreviewTests { extension PreviewTests {
// MARK: - PreviewProvider // MARK: - PreviewProvider
func testBigIcon() async throws { @Test("BigIcon")
func bigIcon() async throws {
for (index, preview) in BigIcon_Previews._allPreviews.enumerated() { for (index, preview) in BigIcon_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index) try await assertSnapshots(matching: preview, step: index)
} }
} }
func testCompoundButtonStyle() async throws { @Test("CompoundButtonStyle")
func compoundButtonStyle() async throws {
for (index, preview) in CompoundButtonStyle_Previews._allPreviews.enumerated() { for (index, preview) in CompoundButtonStyle_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index) try await assertSnapshots(matching: preview, step: index)
} }
} }
func testCompoundIcon() async throws { @Test("CompoundIcon")
func compoundIcon() async throws {
for (index, preview) in CompoundIcon_Previews._allPreviews.enumerated() { for (index, preview) in CompoundIcon_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index) try await assertSnapshots(matching: preview, step: index)
} }
} }
func testCompoundToggleStyle() async throws { @Test("CompoundToggleStyle")
func compoundToggleStyle() async throws {
for (index, preview) in CompoundToggleStyle_Previews._allPreviews.enumerated() { for (index, preview) in CompoundToggleStyle_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index) try await assertSnapshots(matching: preview, step: index)
} }
} }
func testListInlinePicker() async throws { @Test("ListInlinePicker")
func listInlinePicker() async throws {
for (index, preview) in ListInlinePicker_Previews._allPreviews.enumerated() { for (index, preview) in ListInlinePicker_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index) try await assertSnapshots(matching: preview, step: index)
} }
} }
func testListRowAccessory() async throws { @Test("ListRowAccessory")
func listRowAccessory() async throws {
for (index, preview) in ListRowAccessory_Previews._allPreviews.enumerated() { for (index, preview) in ListRowAccessory_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index) try await assertSnapshots(matching: preview, step: index)
} }
} }
func testListRowButtonStyle() async throws { @Test("ListRowButtonStyle")
func listRowButtonStyle() async throws {
for (index, preview) in ListRowButtonStyle_Previews._allPreviews.enumerated() { for (index, preview) in ListRowButtonStyle_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index) try await assertSnapshots(matching: preview, step: index)
} }
} }
func testListRowLabel() async throws { @Test("ListRowLabel")
func listRowLabel() async throws {
for (index, preview) in ListRowLabel_Previews._allPreviews.enumerated() { for (index, preview) in ListRowLabel_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index) try await assertSnapshots(matching: preview, step: index)
} }
} }
func testListRowLoadingSelection() async throws { @Test("ListRowLoadingSelection")
func listRowLoadingSelection() async throws {
for (index, preview) in ListRowLoadingSelection_Previews._allPreviews.enumerated() { for (index, preview) in ListRowLoadingSelection_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index) try await assertSnapshots(matching: preview, step: index)
} }
} }
func testListRowTrailingSection() async throws { @Test("ListRowTrailingSection")
func listRowTrailingSection() async throws {
for (index, preview) in ListRowTrailingSection_Previews._allPreviews.enumerated() { for (index, preview) in ListRowTrailingSection_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index) try await assertSnapshots(matching: preview, step: index)
} }
} }
func testListRow() async throws { @Test("ListRow")
func listRow() async throws {
for (index, preview) in ListRow_Previews._allPreviews.enumerated() { for (index, preview) in ListRow_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index) try await assertSnapshots(matching: preview, step: index)
} }
} }
func testListTextStyles() async throws { @Test("ListTextStyles")
func listTextStyles() async throws {
for (index, preview) in ListTextStyles_Previews._allPreviews.enumerated() { for (index, preview) in ListTextStyles_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index) try await assertSnapshots(matching: preview, step: index)
} }
} }
func testScaledFrameModifier() async throws { @Test("ScaledFrameModifier")
func scaledFrameModifier() async throws {
for (index, preview) in ScaledFrameModifier_Previews._allPreviews.enumerated() { for (index, preview) in ScaledFrameModifier_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index) try await assertSnapshots(matching: preview, step: index)
} }
} }
func testScaledOffsetModifier() async throws { @Test("ScaledOffsetModifier")
func scaledOffsetModifier() async throws {
for (index, preview) in ScaledOffsetModifier_Previews._allPreviews.enumerated() { for (index, preview) in ScaledOffsetModifier_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index) try await assertSnapshots(matching: preview, step: index)
} }
} }
func testScaledPaddingModifier() async throws { @Test("ScaledPaddingModifier")
func scaledPaddingModifier() async throws {
for (index, preview) in ScaledPaddingModifier_Previews._allPreviews.enumerated() { for (index, preview) in ScaledPaddingModifier_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index) try await assertSnapshots(matching: preview, step: index)
} }
} }
func testSearchStyle() async throws { @Test("SearchStyle")
func searchStyle() async throws {
for (index, preview) in SearchStyle_Previews._allPreviews.enumerated() { for (index, preview) in SearchStyle_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index) try await assertSnapshots(matching: preview, step: index)
} }
} }
func testSendButton() async throws { @Test("SendButton")
func sendButton() async throws {
for (index, preview) in SendButton_Previews._allPreviews.enumerated() { for (index, preview) in SendButton_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index) try await assertSnapshots(matching: preview, step: index)
} }
} }
func testTitleAndIcon() async throws { @Test("TitleAndIcon")
func titleAndIcon() async throws {
for (index, preview) in TitleAndIcon_Previews._allPreviews.enumerated() { for (index, preview) in TitleAndIcon_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index) try await assertSnapshots(matching: preview, step: index)
} }
@@ -8,36 +8,33 @@
@testable import Compound @testable import Compound
import Foundation import Foundation
import XCTest import Testing
final class OverrideColorTests: XCTestCase { @Suite
func testSwiftUI() async { struct OverrideColorTests {
// For some very weird reason we need this to be async, `@MainActor` is not enough @Test("SwiftUI color override")
// it will compile but when running it will crash at the end of the run due to some deinit problems. func swiftUI() {
// The other solution would be to make CompoundColors nonisolated but we don't really need that. let colors = CompoundColors()
await MainActor.run { let tokens = CompoundColorTokens()
let colors = CompoundColors() #expect(colors.textPrimary == tokens.textPrimary)
let tokens = CompoundColorTokens()
XCTAssertEqual(colors.textPrimary, tokens.textPrimary)
colors.override(\.textPrimary, with: .pink)
XCTAssertEqual(colors.textPrimary, .pink)
colors.override(\.textPrimary, with: nil)
XCTAssertEqual(colors.textPrimary, tokens.textPrimary)
}
}
/// UIColors are nonisolated, so this is fine.
func testUIKit() {
let colors = CompoundUIColors()
let tokens = CompoundUIColorTokens()
XCTAssertEqual(colors.textPrimary, tokens.textPrimary)
colors.override(\.textPrimary, with: .systemPink) colors.override(\.textPrimary, with: .pink)
XCTAssertEqual(colors.textPrimary, .systemPink) #expect(colors.textPrimary == .pink)
colors.override(\.textPrimary, with: nil) colors.override(\.textPrimary, with: nil)
XCTAssertEqual(colors.textPrimary, tokens.textPrimary) #expect(colors.textPrimary == tokens.textPrimary)
}
@Test("UIKit color override")
func uiKit() {
let colors = CompoundUIColors()
let tokens = CompoundUIColorTokens()
#expect(colors.textPrimary == tokens.textPrimary)
colors.override(\.textPrimary, with: .systemPink)
#expect(colors.textPrimary == .systemPink)
colors.override(\.textPrimary, with: nil)
#expect(colors.textPrimary == tokens.textPrimary)
} }
} }
@@ -8,12 +8,12 @@
import Combine import Combine
@testable import Compound @testable import Compound
@preconcurrency @testable import SnapshotTesting @testable import SnapshotTesting
import SwiftUI import SwiftUI
import XCTest import Testing
@MainActor @Suite(.serialized)
class PreviewTests: XCTestCase { struct PreviewTests {
private struct SnapshotDevice { private struct SnapshotDevice {
let name: String let name: String
let device: String let device: String
@@ -28,17 +28,13 @@ class PreviewTests: XCTestCase {
.init(name: "iPad", device: "iPad")] .init(name: "iPad", device: "iPad")]
private var recordMode: SnapshotTestingConfiguration.Record = .missing private var recordMode: SnapshotTestingConfiguration.Record = .missing
override func setUp() async throws { init() {
try await super.setUp() if ProcessInfo().environment["RECORD_FAILURES"].map(Bool.init) == true {
recordMode = .failed
await MainActor.run {
if ProcessInfo().environment["RECORD_FAILURES"].map(Bool.init) == true {
recordMode = .failed
}
checkEnvironments()
UIView.setAnimationsEnabled(false)
} }
checkEnvironments()
UIView.setAnimationsEnabled(false)
} }
/// Check environments to avoid problems with snapshots on different devices or OS. /// Check environments to avoid problems with snapshots on different devices or OS.
@@ -72,8 +68,7 @@ class PreviewTests: XCTestCase {
let imageRenderer = ImageRenderer(content: preferenceReadingView) let imageRenderer = ImageRenderer(content: preferenceReadingView)
_ = imageRenderer.uiImage _ = imageRenderer.uiImage
var sanitizedSuiteName = String(testName.suffix(testName.count - "test".count).dropLast(2)) let sanitizedSuiteName = String(testName.dropLast(2))
sanitizedSuiteName = sanitizedSuiteName.prefix(1).lowercased() + sanitizedSuiteName.dropFirst()
for snapshotDevice in snapshotDevices { for snapshotDevice in snapshotDevices {
guard var device = PreviewDevice(rawValue: snapshotDevice.device).snapshotDevice() else { guard var device = PreviewDevice(rawValue: snapshotDevice.device).snapshotDevice() else {
@@ -102,7 +97,7 @@ class PreviewTests: XCTestCase {
testName: sanitizedSuiteName, testName: sanitizedSuiteName,
traits: traits, traits: traits,
preferences: preferences) { preferences: preferences) {
XCTFail(failure) Issue.record(Comment(rawValue: failure))
} }
} }
} }
@@ -142,14 +137,6 @@ class PreviewTests: XCTestCase {
testName: testName) testName: testName)
} }
} }
private func wait(for duration: TimeInterval) {
let expectation = XCTestExpectation(description: "Wait")
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
expectation.fulfill()
}
_ = XCTWaiter.wait(for: [expectation], timeout: duration + 1)
}
} }
private class SnapshotPreferences: @unchecked Sendable { private class SnapshotPreferences: @unchecked Sendable {
@@ -1,7 +1,7 @@
// swiftlint:disable all // swiftlint:disable all
// swiftformat:disable all // swiftformat:disable all
import XCTest import Testing
@testable import Compound @testable import Compound
{% if argument.mainTarget %} {% if argument.mainTarget %}
@testable import {{ argument.mainTarget }} @testable import {{ argument.mainTarget }}
@@ -26,7 +26,8 @@ extension PreviewTests {
// MARK: - PreviewProvider // MARK: - PreviewProvider
{% for type in types.types where (type.implements.TestablePreview or type.based.TestablePreview or type|annotated:"TestablePreview") and type.name != "TestablePreview" %} {% for type in types.types where (type.implements.TestablePreview or type.based.TestablePreview or type|annotated:"TestablePreview") and type.name != "TestablePreview" %}
func test{{ type.name|replace:"_Previews", "" }}() async throws { @Test("{{ type.name|replace:"_Previews", "" }}")
func {{ type.name|replace:"_Previews", ""| lowerFirstLetter }}() async throws {
for (index, preview) in {{ type.name }}._allPreviews.enumerated() { for (index, preview) in {{ type.name }}._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index) try await assertSnapshots(matching: preview, step: index)
} }