Skip to content

Commit

Permalink
squash3
Browse files Browse the repository at this point in the history
  • Loading branch information
Lazar Otasevic committed Sep 7, 2024
1 parent 5bca8e5 commit d84c29e
Show file tree
Hide file tree
Showing 14 changed files with 186 additions and 12,488 deletions.
118 changes: 39 additions & 79 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,124 +1,84 @@
# SwiftUI View Testing Framework
# SwiftUI View Inspection Framework

[![Swift](https://github.com/sisoje/swiftui-native/actions/workflows/swift.yml/badge.svg)](https://github.com/sisoje/swiftui-native/actions/workflows/swift.yml)
[![Swift](https://github.com/sisoje/swiftui-view-inspection/actions/workflows/swift.yml/badge.svg)](https://github.com/sisoje/swiftui-view-inspection/actions/workflows/swift.yml)

## Introduction

This framework provides a powerful solution for testing SwiftUI views, with a particular focus on testing views with changing state. It offers tools for hosting and observing SwiftUI components, enabling developers to verify the correctness and behavior of their user interfaces throughout the view lifecycle.
This framework provides powerful tools for inspecting SwiftUI views in unit tests. It allows developers to examine the structure of their views, access individual elements, and simulate user interactions, all within a testing environment.

## Key Features

- **View Hosting**: APIs for hosting views during tests, ensuring controlled testing environments.
- **State Change Testing**: Easily test views with changing state, including `@State`, `@Binding`, and other property wrappers.
- **Lifecycle Event Testing**: Verify the behavior of views during different lifecycle events such as `task` and `onAppear`.
- **Asynchronous Testing**: Support for testing asynchronous operations in SwiftUI views.
- **Navigation Testing**: Capabilities to test NavigationStack and navigation flow.

## Project Structure

The project is structured as follows:

- `HostApp/`: Contains the host application for testing
- `HostAppMain.swift`: Defines the main app entry point as an extension of ViewHostingApp
- `HostAppTests.swift`: Empty file needed for the test target (all test cases are implemented in the ViewHostingTests framework)
- `Sources/`: Contains the main source code for the framework
- `ViewHostingApp/`: Core hosting functionality
- `ViewHostingTests/`: Testing utilities and extensions
- **View Structure Inspection**: Examine the hierarchy and composition of SwiftUI views.
- **Element Access**: Easily access specific elements within a view for verification.
- **Interaction Simulation**: Simulate user interactions like taps, toggles, and text input.
- **State Inspection**: Verify the state of views and their subcomponents.
- **Modifier Inspection**: Examine and test view modifiers like `onAppear`, `task`, and custom modifiers.

## Installation

Add the following to your `Package.swift` file:

```swift
dependencies: [
.package(url: "https://github.com/sisoje/swiftui-native.git", from: "1.0.0")
.package(url: "https://github.com/sisoje/swiftui-view-inspection.git", from: "1.0.0")
]
```

Then, import the frameworks in your test files:
Then, import the framework in your test files:

```swift
import SwiftUI
import XCTest
@testable import ViewHostingApp
import ViewHostingTests
import SwiftUI
@testable import ViewInspection
```

## Usage Guide

### Testing Navigation
### Inspecting a View

Here's an example of how to test navigation using NavigationStack:
To inspect a view:

```swift
func testNavigation() async throws {
struct One: View {
@State var numbers: [Int] = []
var body: some View {
let _ = postBodyEvaluation()
NavigationStack(path: $numbers) {
Text("One")
.navigationDestination(
for: Int.self,
destination: Two.init
)
}
}
}

struct Two: View {
let number: Int
var body: some View {
let _ = postBodyEvaluation()
Text(number.description)
}
}

let one = try await One.hostedView { One() }
one.numbers.append(1)
try await Two.onBodyEvaluation()
}
let view = Text("Hello, World!")
let snapshot = view.snapshot

XCTAssertEqual(snapshot.oneElement(TestElement.View._Text.self).string, "Hello, World!")
```

### Testing Task
### Testing Interactions

Test asynchronous operations using the `task` modifier:
To test a button tap:

```swift
@MainActor func testTask() async throws {
struct DummyView: View {
@State var number = 0
var body: some View {
let _ = postBodyEvaluation()
Text(number.description).task { number = number + 1 }
}
}

let view = try await DummyView.hostedView { DummyView() }
XCTAssertEqual(view.number, 0)
try await DummyView.onBodyEvaluation()
XCTAssertEqual(view.number, 1)
}
var b = false
Button("") { b = true }.snapshot.oneElement(TestElement.View._Button.self).tap()
XCTAssertEqual(b, true)
```

## Advanced Features
### Examining Modifiers

- **Body Evaluation Observation**: Use `postBodyEvaluation()` and `onBodyEvaluation()` to track view updates.
- **Hosted View Testing**: Utilize `hostedView` for testing views with internal state in a controlled environment.
To test an `task` modifier:

## Future Development
```swift
var x = 0
await EmptyView().task { x = 1 }.snapshot.oneElement(TestElement.Modifier._Task.self).doTask()
XCTAssertEqual(x, 1)
```

## Advanced Features

We are planning to develop ViewInspection features, which will provide more detailed introspection capabilities for SwiftUI views. Stay tuned for updates!
- **Reflection-based Inspection**: Uses Swift's reflection capabilities to inspect view structures.
- **Type-safe Element Access**: Strongly-typed access to view elements and modifiers.
- **Comprehensive SwiftUI Support**: Covers a wide range of SwiftUI components and modifiers.

## Contributing

We welcome contributions! Please follow these steps:
Contributions are welcome! Please follow these steps:

1. Fork the repository and create your branch from `main`.
2. Ensure your code follows the existing style and architecture.
3. Write unit tests for your changes.
4. Run all tests to ensure they pass.
5. Submit a pull request with a clear description of your changes.
2. Add or update tests for any new or changed functionality.
3. Ensure your code follows the existing style and passes all tests.
4. Create a pull request with a clear description of your changes.

## License

Expand Down
56 changes: 56 additions & 0 deletions Sources/ViewInspection/Elements/ReflectionElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,59 @@ protocol ReflectionElement {
init(node: ReflectionNode)
static func isValid(_ node: ReflectionNode) -> Bool
}

protocol TypeDerivedElement: ReflectionElement {
associatedtype RelatedType
}

extension TypeDerivedElement {
static var typeInfo: TypeInfo { TypeInfo(type: RelatedType.self) }
}

protocol CastableTypeDerivedElement: TypeDerivedElement {
var castValue: RelatedType { get }
}

extension CastableTypeDerivedElement {
var castValue: RelatedType { CastingUtils.memoryCast(node.object) }
}

struct SameBaseElement<T>: TypeDerivedElement {
let node: ReflectionNode
typealias RelatedType = T
static func isValid(_ node: ReflectionNode) -> Bool { node.typeInfo.baseTypename == typeInfo.baseTypename }
}

protocol ModifierDerivedElement: ReflectionElement {
static func makeModifiedContent() -> Any
}

extension ModifierDerivedElement {
static func isValid(_ node: ReflectionNode) -> Bool {
let modChild = ReflectionNode(object: makeModifiedContent()).children[1]
return node.typeInfo.typename == modChild.typeInfo.typename
}
}

struct SameTypeElement<T>: CastableTypeDerivedElement {
let node: ReflectionNode
typealias RelatedType = T
static func isValid(_ node: ReflectionNode) -> Bool { node.object is T }
}

struct ClosureElement<T>: CastableTypeDerivedElement {
let node: ReflectionNode
typealias RelatedType = T
static func isValid(_ node: ReflectionNode) -> Bool { node.typeInfo.typename.hasSuffix(typeInfo.typename) }
}

struct SomeClosureElement: ReflectionElement {
let node: ReflectionNode
static func isValid(_ node: ReflectionNode) -> Bool {
node.typeInfo.typename.contains("->")
}

func cast<T>(_ t: T.Type = T.self) -> T {
CastingUtils.memoryCast(node.object, T.self)
}
}
Loading

0 comments on commit d84c29e

Please sign in to comment.