Contents

szpakkamil/harnesskit

A structured navigation harness for SwiftUI demo apps and component previews. Define a tree of views using `PathProject` and `PathFolder`, render it with `HarnessView`, then navigate to any screen in UI tests with a single call.

Table of Contents

Overview

  • Define a PathProject as the root of your view hierarchy.
  • Organize views into PathFolder sections and subsections.
  • Render the entire tree with HarnessView<YourProject>.
  • Navigate to any view in UI tests via someCase.navigate(app:).

Implementation

HarnessKit uses two protocols: PathProject for the root and PathFolder for each section.

Define a Project

import HarnessKit

struct MyProject: PathProject {
    static let name = "My App"
    static let folders: [any PathFolder.Type] = [ButtonsFolder.self, TextFolder.self]
}

Define Folders

import SwiftUI
import HarnessKit

enum ButtonsFolder: PathFolder {
    typealias ParentSection = MyProject
    static let name = "Buttons"
    static let folders: [any PathFolder.Type] = []

    case primary
    case secondary
    case destructive

    var description: String {
        switch self {
        case .primary:     "Primary Button"
        case .secondary:   "Secondary Button"
        case .destructive: "Destructive Button"
        }
    }

    @ViewBuilder
    var view: some View {
        switch self {
        case .primary:     PrimaryButtonPreview()
        case .secondary:   SecondaryButtonPreview()
        case .destructive: DestructiveButtonPreview()
        }
    }
}

Render the Harness

import SwiftUI
import HarnessKit

@main
struct HarnessApp: App {
    var body: some Scene {
        WindowGroup {
            HarnessView<MyProject>()
        }
    }
}

HarnessView builds a NavigationStack from your project tree. Tap any folder to drill down; tap any option to open the associated view.

UI Testing

Add HarnessKitTesting to your UI test target, then call navigate(app:) on any folder case.

import XCTest
import HarnessKitTesting

class ButtonTests: XCTestCase {
    func testDestructiveButtonAppears() {
        let app = XCUIApplication()
        app.launch()

        ButtonsFolder.destructive.navigate(app: app)

        XCTAssertTrue(app.buttons["Delete"].exists)
    }
}

navigate(app:) resolves the full path from the project root and taps each level in sequence. On tvOS it uses XCUIRemote directional presses instead of tap gestures.

Resources

Installation

Swift Package Manager

Add HarnessKit as a package dependency in your Package.swift file.

dependencies: [
    .package(url: "https://github.com/SzpakKamil/HarnessKit.git", from: "1.0.0")
]

Add HarnessKit to your harness app target and HarnessKitTesting to your UI test target:

targets: [
    .target(
        name: "MyHarnessApp",
        dependencies: ["HarnessKit"]
    ),
    .testTarget(
        name: "MyHarnessAppUITests",
        dependencies: ["HarnessKitTesting"]
    )
]

Requirements

  • Platforms: iOS 14.0+, macOS 11.0+, tvOS 14.0+, watchOS 10.0+, visionOS 1.0+
  • Tools: Swift 5.9+, Xcode 15.0+

License

HarnessKit is released under the MIT license.

Third-Party Notices

The device bezel images fetched at runtime by HarnessKitTransform depict Apple hardware and are the intellectual property of Apple Inc. They are used in accordance with Apple's App Store Marketing Guidelines and may only be used to frame screenshots of your own application for App Store promotion. See NOTICE.md for full details.

Package Metadata

Repository: szpakkamil/harnesskit

Default branch: master

README: README.md