macos-e2e-scaffold
Manual-invocation skill that bootstraps XCUITest infrastructure for macOS SwiftUI projects.
Phase 0 — Self-check
Before any other action, run three refuse-conditions. Any failure → return early with explicit message; no files modified.
| Check | Detect via | Refuse-message |
|---|---|---|
| Swift project | *.xcodeproj directory or Package.swift in cwd | "Not a Swift project. /macos-e2e-scaffold requires .xcodeproj or Package.swift in project root." |
| SwiftUI macOS app | grep WindowGroup|Window(|Settings {|MenuBarExtra( in *.swift under source root | "No SwiftUI macOS app target detected. Skill is macOS-only — for iOS use /ios-e2e-scaffold (deferred), for AppKit use /appkit-e2e-scaffold (deferred)." |
| Not already scaffolded | <App>UITests/ exists with find ... -name '*.swift' | wc -l > 1 | "UI test target already exists with N test files. Skill won't overwrite — extend manually instead." |
Always emit Phase 0 result on success:
## /macos-e2e-scaffold Phase 0
✅ Swift project detected (<project>.xcodeproj | Package.swift)
✅ SwiftUI macOS app (WindowGroup found in <File.swift>:<line>)
✅ No existing UI test target
Project type: <xcodegen-managed | SPM-based | plain .xcodeproj>
Scheme: <SchemeName>
Source root: <path>
Total .swift files in source root: <N>
Proceeding with audit + scaffold.
What this skill does
- Audits the project: walks the SwiftUI Scene tree, ranks views by interactive-control density, identifies top 5.
- Suggests accessibility identifiers for each control in top 5 views; applies them after user batch-confirmation.
- Generates ranked TIER-1/2/3 test stubs with
XCTFail("not implemented")placeholders, an identifier-convention doc, and a Claude-readable xcresult runner script.
What this skill is NOT
- Not a review skill. Does not analyse spec/plan/PRD artefacts. Use
/pitfall-verification(will-it-work?),/quality-review(will-it-feel-premium?), or/macos-native-review(is-it-Apple-native?) for those. - Not a code-quality reviewer. Does not check view-code idioms (use Antoine van der Lee's
swiftui-expert-skill) or unit-test idioms (useswift-testing-expert). - Not iOS-aware. Use
/ios-e2e-scaffold(deferred — IDEAS.md). - Not AppKit-aware. Use
/appkit-e2e-scaffold(deferred). - Not snapshot-aware. Use
/swiftui-snapshot-scaffold(deferred). - Not auto-invoked. Manual
/macos-e2e-scaffoldonly — same model assetup-routing.
Heuristic process (deterministic, Read+Grep based)
Step 1: Detect project type
- If
xcodegen.ymlorproject.ymlexists → xcodegen-managed - Else if
Package.swiftcontains.executableTarget(name:→ SPM-based - Else if
*.xcodeprojexists → plain .xcodeproj
Step 2: Detect scheme name
- xcodegen: read
name:field at root ofxcodegen.yml/project.yml - SPM: read
name:fromPackage(name: ...) - plain .xcodeproj: parse
*.xcodeproj/xcshareddata/xcschemes/*.xcschemefilenames; fallback to project directory name
Step 3: Find source root
- xcodegen: read
targets.<schemename>.sources.pathfrom yml - SPM:
Sources/<TargetName>/ - plain .xcodeproj: parse
project.pbxprojfor main app target's source grouppath =; fallback to<schemename>/
Step 4: Walk Scene tree
- Grep source root:
grep -rn -E 'WindowGroup|Window\(|Settings \{|MenuBarExtra\(' --include='*.swift' - For each Scene file, grep its body for
NavigationLink(destination:,.sheet(content:,.fullScreenCover(content: - Recursively follow destinations to build view-graph (max depth: 5; cycle detection via view-name set)
- For each view in graph, count interactive controls using word-boundary patterns:
\bButton\b\s*[({]\bToggle\b\s*\(\bTextField\b\s*\(\bPicker\b\s*\(\bNavigationLink\b\s*\(
Step 5: Rank views
Sort views by (reference_count + interactive_control_count) descending. Tie-breaker: alphabetical by source-file name, then by line number. Top 5 receive identifier suggestions.
Step 6: Detect TIER mappings
- TIER-1 #1 (Smoke): always (uses Scene-root window-title)
- TIER-1 #2 (Happy-path): pick top-ranked view's primary button. Heuristic:
ButtoncontainingawaitOR action calling a method namedgenerate*/create*/save*/run*/start*. Fallback if no Button matches: pick the first Button in the top-ranked view (by line number); mark the generated stub with comment// HEURISTIC: generic fallback — no save/create/await action matched. Verify this is the right primary action.so user knows to double-check. - TIER-1 #3 (Error-recovery): pick first view (alphabetical by source-file name, then by line number — deterministic) containing
.alert(...),errorMessage,failure, orerror: Error - TIER-2 (Modal): only if
.sheet(isPresented:or.fullScreenCover(isPresented:found in walked tree - TIER-2 (Menubar): only if
.commands { ... }orMenuBarExtra(found - TIER-3 (Multi-window): only if
WindowGroup-count +Window(-count > 1 - TIER-3 (Toolbar): only if
ToolbarItem(count ≥ 2
Step 7: Generate identifier suggestions
For each control in top 5 views:
- Skip controls that already have
.accessibilityIdentifier(...)set — check next 5 lines after the control declaration. Already-identified controls listed in report under "Already identified (preserved)" but not re-suggested. - Skip controls inside
#Preview { ... }blocks orPreviewProvider(static var previews:) conformances — track brace-depth from#Previeworstatic var previewsdeclarations; exclude when depth > 0. - Construct ID as
<ViewName>_<ControlType>_<Purpose> - Purpose extracted from button label, action method name, or property name (in priority order)
- snake_case all parts;
_separator - Examples:
PlanCardView_Button_GeneratePlan,SettingsView_Toggle_EnableTelemetry,AIChatView_TextField_PromptInput
Step 8: Emit suggestions table for user confirmation
Present batch table in markdown:
| File:line | Current code | Suggested identifier |
|---|---|---|
| PlanCardView.swift:34 | Button("Generate") { ... } | PlanCardView_Button_GeneratePlan |
| ... | ... | ... |
Ask: "Apply all N suggestions? [a]ll / [c]herry-pick / [s]kip"
If [c]herry-pick: follow up with one question per suggestion: "Apply suggestion k of N? [y/n]". Accumulate accepted set; apply only that subset in Step 9.
If [s]kip: skip Step 9 entirely; test files in Step 10 still generated, with placeholder identifier comments showing what user must fill in manually.
Step 9: Apply identifiers
Use Edit tool, one identifier per Edit call. On uniqueness conflict (same ID would land on two distinct controls): skip both; flag for manual review in report.
Step 10: Generate test files
Per TIER, write one .swift file with XCTFail("not implemented — fyll inn assertion") placeholder + TODO-comment pointing to source-file:line + suggested assertions in comments.
File naming:
<App>UITests/SmokeTest.swift(TIER-1 #1)<App>UITests/HappyPathTests.swift(TIER-1 #2)<App>UITests/ErrorRecoveryTests.swift(TIER-1 #3)<App>UITests/ModalAndMenuTests.swift(TIER-2 if any)<App>UITests/MultiWindowAndToolbarTests.swift(TIER-3 if any)
Step 11: Generate runner script
Write scripts/run-uitests.sh per template in §Runner-script-template (substitute <APP> with detected scheme name). Make executable: chmod +x scripts/run-uitests.sh.
Step 12: Generate identifier convention doc
Write docs/accessibility-identifiers.md with the convention, examples, rationale, and a table listing all applied identifiers with their source-file:line.
Step 13: Emit final report
Per Output-format section below.
TIER rubric
| Tier | Always-generate? | Heuristic trigger | Test file |
|---|---|---|---|
| 1 #1 Smoke | yes | always | SmokeTest.swift |
| 1 #2 Happy-path | yes | top-ranked view + primary action | `HappyPathTests |