fosmvvm-ui-tests-generator

ClawSkills 作者 clawskills

Generate UI tests for FOSMVVM SwiftUI views using XCTest and FOSTestingUI. Covers accessibility identifiers, ViewModelOperations, and test data transport.

安装 / 下载方式

TotalClaw CLI推荐
totalclaw install clawskills:clawskills~foscomputerservices-fosmvvm-ui-tests-generator
cURL直接下载,无需登录
curl -fsSL https://skills.taituai.com/api/skills/clawskills%3Aclawskills~foscomputerservices-fosmvvm-ui-tests-generator/file -o foscomputerservices-fosmvvm-ui-tests-generator.md
# FOSMVVM UI Tests Generator

Generate comprehensive UI tests for ViewModelViews in FOSMVVM applications.

## Conceptual Foundation

> For full architecture context, see [FOSMVVMArchitecture.md](../../docs/FOSMVVMArchitecture.md) | [OpenClaw reference]({baseDir}/references/FOSMVVMArchitecture.md)

UI testing in FOSMVVM follows a specific pattern that leverages:
- **FOSTestingUI** framework for test infrastructure
- **ViewModelOperations** for verifying business logic was invoked
- **Accessibility identifiers** for finding UI elements
- **Test data transporter** for passing operation stubs to the app

```
┌─────────────────────────────────────────────────────────────┐
│                    UI Test Architecture                      │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  Test File (XCTest)                 App Under Test          │
│  ┌──────────────────┐              ┌──────────────────┐     │
│  │ MyViewUITests    │              │ MyView           │     │
│  │                  │              │                  │     │
│  │ presentView() ───┼─────────────►│ Show view with   │     │
│  │   with stub VM   │              │   stubbed data   │     │
│  │                  │              │                  │     │
│  │ Interact via ────┼─────────────►│ UI elements with │     │
│  │   identifiers    │              │   .uiTestingId   │     │
│  │                  │              │                  │     │
│  │ Assert on UI     │              │ .testData────────┼──┐  │
│  │   state          │              │   Transporter    │  │  │
│  │                  │              └──────────────────┘  │  │
│  │ viewModelOps() ◄─┼─────────────────────────────────────┘  │
│  │   verify calls   │              Stub Operations          │
│  └──────────────────┘                                        │
│                                                              │
└─────────────────────────────────────────────────────────────┘
```

## Core Components

### 1. Base Test Case Class

Every project should have a base test case that inherits from `ViewModelViewTestCase`:

```swift
class MyAppViewModelViewTestCase<VM: ViewModel, VMO: ViewModelOperations>:
    ViewModelViewTestCase<VM, VMO>, @unchecked Sendable {

    @MainActor func presentView(
        configuration: TestConfiguration,
        viewModel: VM = .stub(),
        timeout: TimeInterval = 3
    ) throws -> XCUIApplication {
        try presentView(
            testConfiguration: configuration.toJSON(),
            viewModel: viewModel,
            timeout: timeout
        )
    }

    override func setUp() async throws {
        try await super.setUp(
            bundle: Bundle.main,
            resourceDirectoryName: "",
            appBundleIdentifier: "com.example.MyApp"
        )

        continueAfterFailure = false
    }
}
```

**Key points:**
- Generic over `ViewModel` and `ViewModelOperations`
- Wraps FOSTestingUI's `presentView()` with project-specific configuration
- Sets up bundle and app bundle identifier
- `continueAfterFailure = false` stops tests immediately on failure

### 2. Individual UI Test Files

Each ViewModelView gets a corresponding UI test file.

**For views WITH operations:**

```swift
final class MyViewUITests: MyAppViewModelViewTestCase<MyViewModel, MyViewOps> {
    // UI Tests - verify UI state
    func testButtonEnabled() async throws {
        let app = try presentView(viewModel: .stub(enabled: true))
        XCTAssertTrue(app.myButton.isEnabled)
    }

    // Operation Tests - verify operations were called
    func testButtonTap() async throws {
        let app = try presentView(configuration: .requireSomeState())
        app.myButton.tap()

        let stubOps = try viewModelOperations()
        XCTAssertTrue(stubOps.myOperationCalled)
    }
}

private extension XCUIApplication {
    var myButton: XCUIElement {
        buttons.element(matching: .button, identifier: "myButtonIdentifier")
    }
}
```

**For views WITHOUT operations** (display-only):

Use an empty stub operations protocol:

```swift
// In your test file
protocol MyViewStubOps: ViewModelOperations {}
struct MyViewStubOpsImpl: MyViewStubOps {}

final class MyViewUITests: MyAppViewModelViewTestCase<MyViewModel, MyViewStubOpsImpl> {
    // UI Tests only - no operation verification
    func testDisplaysCorrectly() async throws {
        let app = try presentView(viewModel: .stub(title: "Test"))
        XCTAssertTrue(app.titleLabel.exists)
    }
}
```

**When to use each:**
- **With operations**: Interactive views that perform actions (forms, buttons that call APIs, etc.)
- **Without operations**: Display-only views (cards, detail views, static content)

### 3. XCUIElement Helper Extensions

Common helpers for interacting with UI elements:

```swift
extension XCUIElement {
    var text: String? {
        value as? String
    }

    func typeTextAndWait(_ string: String, timeout: TimeInterval = 2) {
        typeText(string)
        _ = wait(for: \.text, toEqual: string, timeout: timeout)
    }

    func tapMenu() {
        if isHittable {
            tap()
        } else {
            coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
        }
    }
}
```

### 4. View Requirements

**For views WITH operations:**

```swift
public struct MyView: ViewModelView {
    #if DEBUG
    @State private var repaintToggle = false
    #endif

    private let viewModel: MyViewModel
    private let operations: MyViewModelOperations

    public var body: some View {
        Button(action: doSomething) {
            Text(viewModel.buttonLabel)
        }
        .uiTestingIdentifier("myButtonIdentifier")
        #if DEBUG
        .testDataTransporter(viewModelOps: operations, repaintToggle: $repaintToggle)
        #endif
    }

    public init(viewModel: MyViewModel) {
        self.viewModel = viewModel
        self.operations = viewModel.operations
    }

    private func doSomething() {
        operations.doSomething()
        toggleRepaint()
    }

    private func toggleRepaint() {
        #if DEBUG
        repaintToggle.toggle()
        #endif
    }
}
```

**For views WITHOUT operations** (display-only):

```swift
public struct MyView: ViewModelView {
    private let viewModel: MyViewModel

    public var body: some View {
        VStack {
            Text(viewModel.title)
            Text(viewModel.description)
        }
        .uiTestingIdentifier("mainContent")
    }

    public init(viewModel: MyViewModel) {
        self.viewModel = viewModel
    }
}
```

**Critical patterns (for views WITH operations):**
- `@State private var repaintToggle = false` for triggering test data transport
- `.testDataTransporter(viewModelOps:repaintToggle:)` modifier in DEBUG
- `toggleRepaint()` called after every operation invocation
- `operations` stored as property from `viewModel.operations`

**Display-only views:**
- No `repaintToggle` needed
- No `.testDataTransporter()` modifier needed
- Just add `.uiTestingIdentifier()` to elements you want to test

## ViewModelOperations: Optional

Not all views need ViewModelOperations:

**Views that NEED operations:**
- Forms with submit/cancel actions
- Views that call business logic or APIs
- Interactive views that trigger app state changes
- Views with user-initiated async operations

**Views that DON'T NEED operations:**
- Display-only cards or detail views
- Static content views
- Pure navigation containers
- Server-hosted views that just render data

**For views without operations:**

Create an empty operations file alongside your ViewModel:

```swift
// MyDisplayViewModelOperations.swift
import FOSMVVM
import Foundation

public protocol MyDisplayViewModelOperations: ViewModelOperations {}

#if canImport(SwiftUI)
public final class MyDisplayViewStubOps: MyDisplayViewModelOperations, @unchecked Sendable {
    public init() {}
}
#endif
```

Then use it in tests:

```swift
final class MyDisplayViewUITests: M