From 4f9b9e8a2170b023385a5e5cca2e3139be0b3118 Mon Sep 17 00:00:00 2001 From: Mauro <34335419+Velin92@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:04:04 +0100 Subject: [PATCH] Swift Testing for Compound (#5110) --- compound-ios/Package.swift | 3 + .../CompoundTests/AvatarColorsTests.swift | 11 ++- .../Tests/CompoundTests/FontSizeTests.swift | 94 ++++++++++--------- .../CompoundTests/GeneratedPreviewTests.swift | 56 +++++++---- .../CompoundTests/OverrideColorTests.swift | 51 +++++----- .../Tests/CompoundTests/PreviewTests.swift | 37 +++----- .../Tools/Sourcery/PreviewTests.stencil | 5 +- 7 files changed, 135 insertions(+), 122 deletions(-) diff --git a/compound-ios/Package.swift b/compound-ios/Package.swift index 9022c76a5..94fdb87d0 100644 --- a/compound-ios/Package.swift +++ b/compound-ios/Package.swift @@ -35,6 +35,9 @@ let package = Package( ], exclude: [ "__Snapshots__" + ], + swiftSettings: [ + .defaultIsolation(MainActor.self) ] ) ] diff --git a/compound-ios/Tests/CompoundTests/AvatarColorsTests.swift b/compound-ios/Tests/CompoundTests/AvatarColorsTests.swift index ab6cbee1d..2e1ffff94 100644 --- a/compound-ios/Tests/CompoundTests/AvatarColorsTests.swift +++ b/compound-ios/Tests/CompoundTests/AvatarColorsTests.swift @@ -9,10 +9,10 @@ @testable import Compound import Foundation import SwiftUI -import XCTest +import Testing -@MainActor -final class DecorativeColorsTests: XCTestCase { +@Suite +struct DecorativeColorsTests { struct TestCase { let input: String 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 // https://github.com/element-hq/compound-web/blob/4608dc807c9c904874eac67ff22be3213f4a261d/src/components/Avatar/Avatar.test.tsx#L62 let testCases: [TestCase] = [ @@ -41,7 +42,7 @@ final class DecorativeColorsTests: XCTestCase { ] 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]) } } } diff --git a/compound-ios/Tests/CompoundTests/FontSizeTests.swift b/compound-ios/Tests/CompoundTests/FontSizeTests.swift index 5b6c123ed..752c63a1f 100644 --- a/compound-ios/Tests/CompoundTests/FontSizeTests.swift +++ b/compound-ios/Tests/CompoundTests/FontSizeTests.swift @@ -8,100 +8,106 @@ @testable import Compound import SwiftUI -import XCTest +import Testing @MainActor -final class FontSizeTests: XCTestCase { +@Suite +struct FontSizeTests { /// Test all system text styles to assert mapping between `Font` and `UIFont`. - func testTextStyle() { + @Test("Text style") + func textStyle() { let caption2FontSize = FontSize.reflecting(.caption2) - XCTAssertEqual(caption2FontSize?.value, 11) - XCTAssertEqual(caption2FontSize?.style, .caption2) + #expect(caption2FontSize?.value == 11) + #expect(caption2FontSize?.style == .caption2) let captionFontSize = FontSize.reflecting(.caption) - XCTAssertEqual(captionFontSize?.value, 12) - XCTAssertEqual(captionFontSize?.style, .caption) + #expect(captionFontSize?.value == 12) + #expect(captionFontSize?.style == .caption) let footnoteFontSize = FontSize.reflecting(.footnote) - XCTAssertEqual(footnoteFontSize?.value, 13) - XCTAssertEqual(footnoteFontSize?.style, .footnote) + #expect(footnoteFontSize?.value == 13) + #expect(footnoteFontSize?.style == .footnote) let subheadlineFontSize = FontSize.reflecting(.subheadline) - XCTAssertEqual(subheadlineFontSize?.value, 15) - XCTAssertEqual(subheadlineFontSize?.style, .subheadline) + #expect(subheadlineFontSize?.value == 15) + #expect(subheadlineFontSize?.style == .subheadline) let calloutFontSize = FontSize.reflecting(.callout) - XCTAssertEqual(calloutFontSize?.value, 16) - XCTAssertEqual(calloutFontSize?.style, .callout) + #expect(calloutFontSize?.value == 16) + #expect(calloutFontSize?.style == .callout) let bodyFontSize = FontSize.reflecting(.body) - XCTAssertEqual(bodyFontSize?.value, 17) - XCTAssertEqual(bodyFontSize?.style, .body) + #expect(bodyFontSize?.value == 17) + #expect(bodyFontSize?.style == .body) let headlineFontSize = FontSize.reflecting(.headline) - XCTAssertEqual(headlineFontSize?.value, 17) - XCTAssertEqual(headlineFontSize?.style, .headline) + #expect(headlineFontSize?.value == 17) + #expect(headlineFontSize?.style == .headline) let title3FontSize = FontSize.reflecting(.title3) - XCTAssertEqual(title3FontSize?.value, 20) - XCTAssertEqual(title3FontSize?.style, .title3) + #expect(title3FontSize?.value == 20) + #expect(title3FontSize?.style == .title3) let title2FontSize = FontSize.reflecting(.title2) - XCTAssertEqual(title2FontSize?.value, 22) - XCTAssertEqual(title2FontSize?.style, .title2) + #expect(title2FontSize?.value == 22) + #expect(title2FontSize?.style == .title2) let titleFontSize = FontSize.reflecting(.title) - XCTAssertEqual(titleFontSize?.value, 28) - XCTAssertEqual(titleFontSize?.style, .title) + #expect(titleFontSize?.value == 28) + #expect(titleFontSize?.style == .title) let largeTitleFontSize = FontSize.reflecting(.largeTitle) - XCTAssertEqual(largeTitleFontSize?.value, 34) - XCTAssertEqual(largeTitleFontSize?.style, .largeTitle) + #expect(largeTitleFontSize?.value == 34) + #expect(largeTitleFontSize?.style == .largeTitle) } - func testModifiedTextStyle() { + @Test("Modified text style font sizes") + func modifiedTextStyle() { let boldCaptionFontSize = FontSize.reflecting(.caption.bold()) - XCTAssertEqual(boldCaptionFontSize?.value, 12) - XCTAssertEqual(boldCaptionFontSize?.style, .caption) + #expect(boldCaptionFontSize?.value == 12) + #expect(boldCaptionFontSize?.style == .caption) let styledTitle = Font.title.width(.compressed).bold().italic().monospaced() let styledTitleFontSize = FontSize.reflecting(styledTitle) - XCTAssertEqual(styledTitleFontSize?.value, 28) - XCTAssertEqual(styledTitleFontSize?.style, .title) + #expect(styledTitleFontSize?.value == 28) + #expect(styledTitleFontSize?.style == .title) } - func testSystemFont() { + @Test("System font sizes") + func systemFont() { let system21FontSize = FontSize.reflecting(.system(size: 21)) - XCTAssertEqual(system21FontSize?.value, 21) + #expect(system21FontSize?.value == 21) 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 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)) - XCTAssertEqual(custom43FontSize?.value, 43) - XCTAssertEqual(custom43FontSize?.style, .body) + #expect(custom43FontSize?.value == 43) + #expect(custom43FontSize?.style == .body) let styledCustom35 = Font.custom("Baskerville", size: 35).weight(.thin).monospaced().italic() let styledCustom35FontSize = FontSize.reflecting(styledCustom35) - XCTAssertEqual(styledCustom35FontSize?.value, 35) - XCTAssertEqual(styledCustom35FontSize?.style, .body) + #expect(styledCustom35FontSize?.value == 35) + #expect(styledCustom35FontSize?.style == .body) } - func testCustomFontWithTextStyle() { + @Test("Custom font with text style") + func customFontWithTextStyle() { let customTitle21FontSize = FontSize.reflecting(.custom("Baskerville", size: 21, relativeTo: .title)) - XCTAssertEqual(customTitle21FontSize?.value, 21) - XCTAssertEqual(customTitle21FontSize?.style, .title) + #expect(customTitle21FontSize?.value == 21) + #expect(customTitle21FontSize?.style == .title) let styledCustomFootnote15 = Font.custom("Baskerville", size: 15, relativeTo: .footnote).weight(.thin).monospaced().italic() let styledCustomFootnote15FontSize = FontSize.reflecting(styledCustomFootnote15) - XCTAssertEqual(styledCustomFootnote15FontSize?.value, 15) - XCTAssertEqual(styledCustomFootnote15FontSize?.style, .footnote) + #expect(styledCustomFootnote15FontSize?.value == 15) + #expect(styledCustomFootnote15FontSize?.style == .footnote) } } \ No newline at end of file diff --git a/compound-ios/Tests/CompoundTests/GeneratedPreviewTests.swift b/compound-ios/Tests/CompoundTests/GeneratedPreviewTests.swift index d59b8cc27..1a134c072 100644 --- a/compound-ios/Tests/CompoundTests/GeneratedPreviewTests.swift +++ b/compound-ios/Tests/CompoundTests/GeneratedPreviewTests.swift @@ -4,116 +4,134 @@ // swiftlint:disable all // swiftformat:disable all -import XCTest +import Testing @testable import Compound extension PreviewTests { // MARK: - PreviewProvider - func testBigIcon() async throws { + @Test("BigIcon") + func bigIcon() async throws { for (index, preview) in BigIcon_Previews._allPreviews.enumerated() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { try await assertSnapshots(matching: preview, step: index) } diff --git a/compound-ios/Tests/CompoundTests/OverrideColorTests.swift b/compound-ios/Tests/CompoundTests/OverrideColorTests.swift index 2e6ff41a3..6e78aa9b0 100644 --- a/compound-ios/Tests/CompoundTests/OverrideColorTests.swift +++ b/compound-ios/Tests/CompoundTests/OverrideColorTests.swift @@ -8,36 +8,33 @@ @testable import Compound import Foundation -import XCTest +import Testing -final class OverrideColorTests: XCTestCase { - func testSwiftUI() async { - // For some very weird reason we need this to be async, `@MainActor` is not enough - // it will compile but when running it will crash at the end of the run due to some deinit problems. - // The other solution would be to make CompoundColors nonisolated but we don't really need that. - await MainActor.run { - let colors = CompoundColors() - 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) +@Suite +struct OverrideColorTests { + @Test("SwiftUI color override") + func swiftUI() { + let colors = CompoundColors() + let tokens = CompoundColorTokens() + #expect(colors.textPrimary == tokens.textPrimary) - colors.override(\.textPrimary, with: .systemPink) - XCTAssertEqual(colors.textPrimary, .systemPink) + colors.override(\.textPrimary, with: .pink) + #expect(colors.textPrimary == .pink) 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) } } diff --git a/compound-ios/Tests/CompoundTests/PreviewTests.swift b/compound-ios/Tests/CompoundTests/PreviewTests.swift index fcf544d15..c07e7804c 100644 --- a/compound-ios/Tests/CompoundTests/PreviewTests.swift +++ b/compound-ios/Tests/CompoundTests/PreviewTests.swift @@ -8,12 +8,12 @@ import Combine @testable import Compound -@preconcurrency @testable import SnapshotTesting +@testable import SnapshotTesting import SwiftUI -import XCTest +import Testing -@MainActor -class PreviewTests: XCTestCase { +@Suite(.serialized) +struct PreviewTests { private struct SnapshotDevice { let name: String let device: String @@ -28,17 +28,13 @@ class PreviewTests: XCTestCase { .init(name: "iPad", device: "iPad")] private var recordMode: SnapshotTestingConfiguration.Record = .missing - override func setUp() async throws { - try await super.setUp() - - await MainActor.run { - if ProcessInfo().environment["RECORD_FAILURES"].map(Bool.init) == true { - recordMode = .failed - } - - checkEnvironments() - UIView.setAnimationsEnabled(false) + init() { + if ProcessInfo().environment["RECORD_FAILURES"].map(Bool.init) == true { + recordMode = .failed } + + checkEnvironments() + UIView.setAnimationsEnabled(false) } /// Check environments to avoid problems with snapshots on different devices or OS. @@ -72,8 +68,7 @@ class PreviewTests: XCTestCase { let imageRenderer = ImageRenderer(content: preferenceReadingView) _ = imageRenderer.uiImage - var sanitizedSuiteName = String(testName.suffix(testName.count - "test".count).dropLast(2)) - sanitizedSuiteName = sanitizedSuiteName.prefix(1).lowercased() + sanitizedSuiteName.dropFirst() + let sanitizedSuiteName = String(testName.dropLast(2)) for snapshotDevice in snapshotDevices { guard var device = PreviewDevice(rawValue: snapshotDevice.device).snapshotDevice() else { @@ -102,7 +97,7 @@ class PreviewTests: XCTestCase { testName: sanitizedSuiteName, traits: traits, preferences: preferences) { - XCTFail(failure) + Issue.record(Comment(rawValue: failure)) } } } @@ -142,14 +137,6 @@ class PreviewTests: XCTestCase { 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 { diff --git a/compound-ios/Tools/Sourcery/PreviewTests.stencil b/compound-ios/Tools/Sourcery/PreviewTests.stencil index d706c192d..cc2822d95 100644 --- a/compound-ios/Tools/Sourcery/PreviewTests.stencil +++ b/compound-ios/Tools/Sourcery/PreviewTests.stencil @@ -1,7 +1,7 @@ // swiftlint:disable all // swiftformat:disable all -import XCTest +import Testing @testable import Compound {% if argument.mainTarget %} @testable import {{ argument.mainTarget }} @@ -26,7 +26,8 @@ extension PreviewTests { // MARK: - PreviewProvider {% 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() { try await assertSnapshots(matching: preview, step: index) }