fosmvvm-ui-tests-generator
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-generatorcURL直接下载,无需登录
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