FOSMVVM UI Tests Generator
by @foscomputerservices
Generate UI tests for FOSMVVM SwiftUI views using XCTest and FOSTestingUI. Covers accessibility identifiers, ViewModelOperations, and test data transport.
clawhub install fosmvvm-ui-tests-generatorπ About This Skill
name: fosmvvm-ui-tests-generator description: Generate UI tests for FOSMVVM SwiftUI views using XCTest and FOSTestingUI. Covers accessibility identifiers, ViewModelOperations, and test data transport. homepage: https://github.com/foscomputerservices/FOSUtilities metadata: {"clawdbot": {"emoji": "π₯οΈ", "os": ["darwin"]}}
FOSMVVM UI Tests Generator
Generate comprehensive UI tests for ViewModelViews in FOSMVVM applications.
Conceptual Foundation
> For full architecture context, see FOSMVVMArchitecture.md | OpenClaw reference
UI testing in FOSMVVM follows a specific pattern that leverages:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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:
class MyAppViewModelViewTestCase:
ViewModelViewTestCase, @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:
ViewModel and ViewModelOperationspresentView() with project-specific configurationcontinueAfterFailure = false stops tests immediately on failure2. Individual UI Test Files
Each ViewModelView gets a corresponding UI test file.
For views WITH operations:
final class MyViewUITests: MyAppViewModelViewTestCase {
// 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:
// In your test file
protocol MyViewStubOps: ViewModelOperations {}
struct MyViewStubOpsImpl: MyViewStubOps {}final class MyViewUITests: MyAppViewModelViewTestCase {
// 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:
3. XCUIElement Helper Extensions
Common helpers for interacting with UI elements:
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:
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):
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 DEBUGtoggleRepaint() called after every operation invocationoperations stored as property from viewModel.operationsDisplay-only views:
repaintToggle needed.testDataTransporter() modifier needed.uiTestingIdentifier() to elements you want to testViewModelOperations: Optional
Not all views need ViewModelOperations:
Views that NEED operations:
Views that DON'T NEED operations:
For views without operations:
Create an empty operations file alongside your ViewModel:
// MyDisplayViewModelOperations.swift
import FOSMVVM
import Foundationpublic protocol MyDisplayViewModelOperations: ViewModelOperations {}
#if canImport(SwiftUI)
public final class MyDisplayViewStubOps: MyDisplayViewModelOperations, @unchecked Sendable {
public init() {}
}
#endif
Then use it in tests:
final class MyDisplayViewUITests: MyAppViewModelViewTestCase<
MyDisplayViewModel,
MyDisplayViewStubOps
> {
// Only test UI state, no operation verification
}
The view itself doesn't need:
repaintToggle state.testDataTransporter() modifieroperations propertytoggleRepaint() functionJust add .uiTestingIdentifier() to elements you want to verify.
Test Categories
UI State Tests
Verify that the UI displays correctly based on ViewModel state:
func testButtonDisabledWhenNotReady() async throws {
let app = try presentView(viewModel: .stub(ready: false))
XCTAssertFalse(app.submitButton.isEnabled)
}func testButtonEnabledWhenReady() async throws {
let app = try presentView(viewModel: .stub(ready: true))
XCTAssertTrue(app.submitButton.isEnabled)
}
Operation Tests
Verify that user interactions invoke the correct operations:
func testSubmitButtonInvokesOperation() async throws {
let app = try presentView(configuration: .requireAuth())
app.submitButton.tap() let stubOps = try viewModelOperations()
XCTAssertTrue(stubOps.submitCalled)
XCTAssertFalse(stubOps.cancelCalled)
}
Navigation Tests
Verify navigation flows work correctly:
func testNavigationToDetailView() async throws {
let app = try presentView()
app.itemRow.tap() XCTAssertTrue(app.detailView.exists)
}
When to Use This Skill
What This Skill Generates
Initial Setup (once per project)
| File | Location | Purpose |
|------|----------|---------|
| {ProjectName}ViewModelViewTestCase.swift | Tests/UITests/Support/ | Base test case for all UI tests |
| XCUIElement.swift | Tests/UITests/Support/ | Helper extensions for XCUIElement |
Per ViewModelView
| File | Location | Purpose |
|------|----------|---------|
| {ViewName}ViewModelOperations.swift | Sources/{ViewModelsTarget}/{Feature}/ | Operations protocol and stub (if view has interactions) |
| {ViewName}UITests.swift | Tests/UITests/Views/{Feature}/ | UI tests for the view |
Note: Views without user interactions use an empty operations file with just the protocol and minimal stub.
Project Structure Configuration
| Placeholder | Description | Example |
|-------------|-------------|---------|
| {ProjectName} | Your project/app name | MyApp, TaskManager |
| {ViewName} | The ViewModelView name (without "View" suffix) | TaskList, Dashboard |
| {Feature} | Feature/module grouping | Tasks, Settings |
How to Use This Skill
Invocation: /fosmvvm-ui-tests-generator
Prerequisites:
Workflow integration: This skill is typically used after implementing ViewModelViews. The skill references conversation context automaticallyβno file paths or Q&A needed. Often follows fosmvvm-swiftui-view-generator or fosmvvm-react-view-generator.
Pattern Implementation
This skill references conversation context to determine test structure:
Test Type Detection
From conversation context, the skill identifies:
View Analysis
From requirements already in context:
Infrastructure Planning
Based on project state:
Test File Generation
For the specific view: 1. Test class inheriting from base test case 2. UI state tests (verify display based on ViewModel) 3. Operation tests (verify user interactions invoke operations) 4. XCUIApplication extension with element accessors
View Requirements
Ensure test identifiers and data transport:
1. .uiTestingIdentifier() on all interactive elements
2. @State private var repaintToggle (if has operations)
3. .testDataTransporter() modifier (if has operations)
4. toggleRepaint() calls after operations (if has operations)
Context Sources
Skill references information from:
Key Patterns
Test Configuration Pattern
Use TestConfiguration for tests that need specific app state:
func testWithSpecificState() async throws {
let app = try presentView(
configuration: .requireAuth(userId: "123")
)
// Test with authenticated state
}
Element Accessor Pattern
Define element accessors in a private extension:
private extension XCUIApplication {
var submitButton: XCUIElement {
buttons.element(matching: .button, identifier: "submitButton")
} var cancelButton: XCUIElement {
buttons.element(matching: .button, identifier: "cancelButton")
}
var firstItem: XCUIElement {
buttons.element(matching: .button, identifier: "itemButton").firstMatch
}
}
Operation Verification Pattern
After user interactions, verify operations were called:
func testDecrementButton() async throws {
let app = try presentView(configuration: .requireDevice())
app.decrementButton.tap() let stubOps = try viewModelOperations()
XCTAssertTrue(stubOps.decrementCalled)
XCTAssertFalse(stubOps.incrementCalled)
}
Orientation Setup Pattern
Set device orientation in setUp() if needed:
override func setUp() async throws {
try await super.setUp() #if os(iOS)
XCUIDevice.shared.orientation = .portrait
#endif
}
View Testing Checklist
All views:
.uiTestingIdentifier() on all elements you want to testViews WITH operations (interactive views):
@State private var repaintToggle = false property.testDataTransporter(viewModelOps:repaintToggle:) modifiertoggleRepaint() helper functiontoggleRepaint() called after every operation invocationoperations stored from viewModel.operations in initViews WITHOUT operations (display-only):
repaintToggle needed.testDataTransporter() neededoperations property neededoperations stored from viewModel.operations in initCommon Test Patterns
Testing Async Operations
func testAsyncOperation() async throws {
let app = try presentView()
app.loadButton.tap() // Wait for UI to update
_ = app.waitForExistence(timeout: 3)
let stubOps = try viewModelOperations()
XCTAssertTrue(stubOps.loadCalled)
}
Testing Form Input
func testFormInput() async throws {
let app = try presentView() let emailField = app.emailTextField
emailField.tap()
emailField.typeTextAndWait("user@example.com")
app.submitButton.tap()
let stubOps = try viewModelOperations()
XCTAssertTrue(stubOps.submitCalled)
}
Testing Error States
func testErrorDisplay() async throws {
let app = try presentView(viewModel: .stub(hasError: true)) XCTAssertTrue(app.errorAlert.exists)
XCTAssertEqual(app.errorMessage.text, "An error occurred")
}
File Templates
See reference.md for complete file templates.
Naming Conventions
| Concept | Convention | Example |
|---------|------------|---------|
| Base test case | {ProjectName}ViewModelViewTestCase | MyAppViewModelViewTestCase |
| UI test file | {ViewName}UITests | TaskListViewUITests |
| Test method (UI state) | test{Condition} | testButtonEnabled |
| Test method (operation) | test{Action} | testSubmitButton |
| Element accessor | {elementName} | submitButton, emailTextField |
| UI testing identifier | {elementName}Identifier or {elementName} | "submitButton", "emailTextField" |
See Also
Version History
| Version | Date | Changes | |---------|------|---------| | 1.0 | 2026-01-23 | Initial skill for UI tests | | 1.1 | 2026-01-24 | Update to context-aware approach (remove file-parsing/Q&A). Skill references conversation context instead of asking questions or accepting file paths. |