Skip to content
Back to skills

Emacs Transient

ASecurity

Expert guide for building Emacs transient menus (keyboard-driven UI like Magit). Use when implementing or debugging transient-define-prefix, transient-define-suffix, or transient-define-infix commands.

  • 666 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added February 8, 2026
documentationgotestingdebugginggitperformancedocumentation

Security analysis

A100/100

Scanned February 12, 2026

npx -y skills add majiayu000/claude-skill-registry --skill emacs-transient --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Emacs Transient?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Emacs Transient
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/majiayu000-emacs-transient/badge)](https://www.skillsdirectory.com/skills/majiayu000-emacs-transient)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: emacs-transient
description: Expert guide for building Emacs transient menus (keyboard-driven UI like Magit). Use when implementing or debugging transient-define-prefix, transient-define-suffix, or transient-define-infix commands.
---

# Emacs Transient Expert

This skill provides comprehensive guidance for building transient-based
interfaces in Emacs Lisp, based on the official transient library and
real-world implementations in Magit, Forge, and transient-showcase.

## When to Use This Skill

Invoke this skill when:
- Implementing new `transient-define-prefix` menus
- Creating `transient-define-suffix` or `transient-define-infix` commands
- Debugging transient menu layouts or behavior
- Understanding transient levels, groups, or conditional display
- Working on keyboard-driven UI similar to Magit
- Converting traditional Emacs commands to transient interfaces

## Core Concepts

### What is Transient?

Transient is a library for creating keyboard-driven, temporary menus
in Emacs. It's the foundation of Magit's interface and provides:
- **Display of current state** - Show active arguments and options
- **Modal bindings** - Temporary keymaps that disappear when done
- **Contextual UI** - Menus adapt based on state
- **Persistence** - Save and restore argument values across sessions
- **History** - Track previously used values

### Key Terminology

- **Prefix** - The main transient command that opens the menu
- **Suffix** - Commands invoked from the transient (actions)
- **Infix** - Special suffixes that set arguments/options without
  exiting
- **Groups** - Organizational units for layout (rows, columns,
  sections)
- **Levels** (1-7) - Control visibility based on user expertise
  (default: 4)
- **Scope** - Contextual value passed to suffixes (e.g., current
  branch)

## Three Core Macros

### 1. transient-define-prefix

Defines the main transient menu.

```elisp
(transient-define-prefix my-menu ()
  "Description of what this menu does."
  :man-page "git-commit"              ; Optional: link to man page
  :info-manual "(magit)Committing"    ; Optional: link to info manual
  :value '("--verbose")               ; Optional: default arguments

  ;; Groups define layout
  ["Arguments"                        ; Group header
   ("-v" "Verbose" "--verbose")       ; Switch (toggle)
   ("-a" "Author" "--author="         ; Option (takes value)
    :prompt "Author: ")
   (my-custom-infix)]                 ; Reference to defined infix

  [["Actions"                         ; Nested groups = columns
    ("c" "Commit" my-commit-cmd)
    ("a" "Amend" my-amend-cmd)]
   ["Other"
    ("q" "Quit" transient-quit)]])
```

**Key slots:**
- `:value` - Default arguments
- `:man-page` - Man page for help
- `:info-manual` - Info manual section
- `:transient-suffix` - Default transient behavior for suffixes
- `:transient-non-suffix` - Allow/forbid non-suffix commands
- `:refresh-suffixes` - When to refresh suffix state

### 2. transient-define-suffix

Defines action commands (suffixes).

```elisp
(transient-define-suffix my-commit-cmd (args)
  "Create a commit with ARGS."
  :description "Commit staged changes"  ; Optional: override in menu
  :transient t                          ; Stay transient after calling
  (interactive (list (transient-args 'my-menu)))
  (apply #'my-run-git "commit" args))
```

**Key slots:**
- `:key` - Key binding (can override menu binding)
- `:description` - Can be string or function returning string
- `:transient` - Control transient state (see below)
- `:if` / `:if-not` - Conditional visibility
- `:inapt-if` / `:inapt-if-not` - Show but disable

### 3. transient-define-infix (or transient-define-argument)

Defines argument commands (infixes).

```elisp
(transient-define-argument my-author-arg ()
  :description "Set author"
  :class 'transient-option              ; Option class (takes value)
  :shortarg "-a"                        ; Short form
  :argument "--author="                 ; Long form
  :reader #'my-read-author)             ; Custom reader function

(transient-define-infix my-verbose-switch ()
  :description "Verbose output"
  :class 'transient-switch              ; Switch class (boolean)
  :argument "--verbose")
```

## Infix/Suffix Classes

### Suffix Classes

- **`transient-suffix`** - Base class for all suffixes
- **`transient-infix`** - Base for all infixes (auto-stays transient)

### Infix Classes (derive from transient-infix)

**For command-line arguments:**
- **`transient-switch`** - Boolean flag (e.g., `--verbose`)
- **`transient-option`** - Argument with value (e.g., `--author=NAME`)
- **`transient-switches`** - Mutually exclusive options
- **`transient-files`** - File arguments (`--` separator)

**For variables:**
- **`transient-variable`** - Base for variable infixes
- **`transient-lisp-variable`** - Set Emacs Lisp variables

**Display only:**
- **`transient-information`** - Display info (no command/key)
- **`transient-information*`** - Info aligned with descriptions

## Suffix Specification Syntax

Three ways to specify suffixes:

### 1. Inline (shorthand)
```elisp
("key" "description" command)
("-s" "switch" "--switch")              ; Auto-creates transient-switch
("-o" "option" "--option=")             ; Auto-creates transient-option
```

### 2. With keyword arguments
```elisp
("key" "description" command
 :transient t                           ; Stay transient
 :if (lambda () (my-condition)))        ; Conditional

("-a" "author" "--author="
 :prompt "Author name: "
 :reader #'my-custom-reader
 :always-read t)                        ; Always prompt, don't toggle
```

### 3. Reference to separately defined command
```elisp
(my-custom-suffix)                      ; Uses suffix's own key/desc
```

## Group Specification Syntax

```elisp
[{LEVEL} {DESCRIPTION} {KEYWORD VALUE}... ELEMENT...]
```

**Common patterns:**

```elisp
["Group Title"                          ; Simple group with title
 ("k" "desc" cmd)]

[:description "Dynamic"                 ; Dynamic description
 :description (lambda () (format "Time: %s" (current-time-string)))
 ("k" "desc" cmd)]

[:if some-predicate                     ; Conditional group
 ("k" "desc" cmd)]

[:class transient-row                   ; Explicit layout class
 ("k" "desc" cmd)]

[["Column 1"                            ; Nested groups = columns
  ("a" "cmd a" cmd-a)]
 ["Column 2"
  ("b" "cmd b" cmd-b)]]
```

**Group classes:**
- **`transient-column`** - Stack items vertically (default)
- **`transient-row`** - Arrange items horizontally
- **`transient-columns`** - Contains column groups side-by-side
- **`transient-subgroups`** - Contains subgroups

## The :transient Slot (Controlling State)

Controls whether transient stays active after invoking a command.

**For suffixes (default: exit transient):**
- `nil` or `:transient nil` - Exit transient (default for suffixes)
- `t` or `:transient t` - Stay transient
- `:transient 'transient--do-call` - Export args and stay
- `:transient 'transient--do-return` - Return to parent prefix

**For infixes (default: stay transient):**
- Infixes always use `transient--do-stay` by default
- Rarely need to override

**For sub-prefixes (nested transients):**
- `nil` - Exit all transients when sub-prefix exits
- `t` - Return to parent when sub-prefix exits
- `:transient 'transient--do-recurse` - Enable return behavior
- `:transient 'transient--do-replace` - Replace parent (no return)

**Common pre-commands:**
- `transient--do-exit` - Exit and export args
- `transient--do-stay` - Stay, don't export args
- `transient--do-call` - Stay and export args
- `transient--do-return` - Exit to parent prefix

## Layout Patterns

### Stacked Groups (Vertical)
```elisp
(transient-define-prefix my-menu ()
  ["Top Group" ...]
  ["Bottom Group" ...])
```

### Columns (Side-by-side)
```elisp
(transient-define-prefix my-menu ()
  [["Left Column" ...]
   ["Right Column" ...]])
```

### Mixed (Stacked + Columns)
```elisp
(transient-define-prefix my-menu ()
  ["Top Group (full width)" ...]

  [["Left Column" ...]
   ["Right Column" ...]])
```

### Grid Layout
```elisp
(transient-define-prefix my-menu ()
  [:description "The Grid"
   ["Left Column"
    ("tl" "top-left" cmd)
    ("bl" "bottom-left" cmd)]
   ["Right Column"
    ("tr" "top-right" cmd)
    ("br" "bottom-right" cmd)]])
```

### Spacing
```elisp
["Group"
 ""                                     ; Empty line
 ("k" "first" cmd)
 ("l" "second" cmd)
 ""                                     ; Another gap
 ("m" "third" cmd)]
```

## Accessing Transient Values

### In suffix commands

```elisp
(transient-define-suffix my-suffix (args)
  "Do something with ARGS."
  (interactive (list (transient-args 'my-prefix)))
  ;; Now use args...
  (message "Args: %S" args))
```

### Getting specific argument values

```elisp
(let* ((args (transient-args 'my-prefix))
       (author (transient-arg-value "--author=" args))
       (verbose-p (transient-arg-value "--verbose" args)))
  ;; Use values...
  )
```

### Using scope (contextual value)

```elisp
(transient-define-prefix my-menu (scope)
  "Menu with scope."
  ["Actions"
   ("a" "Action" my-action)]
  (interactive "P")  ; Can take prefix arg as scope
  (transient-setup 'my-menu nil nil :scope scope))

(transient-define-suffix my-action ()
  (interactive)
  (let ((scope (transient-scope)))
    (message "Scope: %S" scope)))
```

## Conditional Display (Predicates)

### Visibility predicates (if suffix should appear)

```elisp
("k" "desc" cmd
 :if (lambda () (file-exists-p "Makefile")))      ; Show if true

("k" "desc" cmd
 :if-not some-mode                                ; Show if not in mode
 :if-non-nil some-variable                        ; Show if var non-nil
 :if-mode 'emacs-lisp-mode                        ; Show in mode
 :if-derived 'prog-mode)                          ; Show if derived
```

### Inapt predicates (show but grayed out)

```elisp
("k" "desc" cmd
 :inapt-if (lambda () (not (magit-anything-staged-p)))) ; Gray if true

("k" "desc" cmd
 :inapt-if-not some-function                      ; Gray if false
 :inapt-if-nil some-variable)                     ; Gray if var nil
```

### On groups

```elisp
[:if magit-rebase-in-progress-p         ; Whole group conditional
 ("a" "abort" magit-rebase-abort)
 ("c" "continue" magit-rebase-continue)]
```

## Important Suffix Slots

**Required/Common:**
- `:key` - Key binding
- `:description` - String or function returning string
- `:command` - The command to invoke

**Behavioral:**
- `:transient` - Stay transient? (t/nil/pre-command)
- `:level` - Visibility level (1-7, default 4)

**Conditional:**
- `:if`, `:if-not`, `:if-mode`, `:if-derived` - Visibility
- `:inapt-if`, `:inapt-if-not` - Enable/disable

**Display:**
- `:format` - Custom display format (`%k` `%d` `%v`)
- `:face` - Face for description
- `:summary` - Echo area/tooltip text

**Help:**
- `:show-help` - Custom help function

## Important Infix Slots

**All suffix slots, plus:**

**Argument-related:**
- `:argument` - Long form (e.g., `--verbose`)
- `:shortarg` - Short form (e.g., `-v`)
- `:class` - Infix class (switch/option/etc.)

**Reading values:**
- `:reader` - Function to read value (PROMPT, INITIAL, HISTORY)
- `:prompt` - Prompt string or function
- `:choices` - List of valid values
- `:always-read` - Always prompt (don't toggle for options)
- `:allow-empty` - Allow empty string

**Multi-value:**
- `:multi-value` - `'rest` or `'repeat` for multiple values

**Other:**
- `:init-value` - Function to set initial value
- `:history-key` - Symbol for history (share across infixes)
- `:unsavable` - Don't save with prefix value

## Levels (1-7)

Control visibility based on user preference:
- **1-3**: Essential commands (always visible)
- **4**: Default level
- **5-6**: Advanced/rarely used
- **7**: Experimental/debug
- **0**: Never show (effectively disabled)

```elisp
("k" "advanced" cmd :level 6)           ; Only show at level 6+

["Arguments" :level 5                   ; Whole group at level 5
 ("-v" "verbose" "--verbose")]
```

Users can change levels interactively with `C-x l`.

## Dynamic Content

### Dynamic descriptions

```elisp
("k" my-cmd
 :description (lambda ()
                (format "Branch: %s" (magit-get-current-branch))))
```

### Dynamic groups (:setup-children)

```elisp
[:class transient-column
 :setup-children
 (lambda (_)
   (transient-parse-suffixes
    'my-prefix
    (mapcar (lambda (file)
              (list (substring file 0 1) file
                    (lambda () (interactive) (find-file file))))
            (directory-files "."))))]
```

### Information display

```elisp
["Info"
 (:info "Static information")
 (:info (lambda () (format "Dynamic: %s" (current-time-string))))
 (:info my-info-function :format " %d")]  ; Custom format
```

## Common Patterns from Magit

### Context-aware suffixes

```elisp
(transient-define-suffix my-cmd (args)
  (interactive
   (if (derived-mode-p 'my-list-mode)
       (list (my-get-args-from-buffer))
     (list (transient-args 'my-prefix))))
  ...)
```

### Shared argument groups

```elisp
(transient-define-group my-common-args ()
  ["Common Arguments"
   ("-v" "Verbose" "--verbose")
   ("-q" "Quiet" "--quiet")])

(transient-define-prefix my-menu-1 ()
  [my-common-args]                      ; Include by reference
  ["Actions" ...])

(transient-define-prefix my-menu-2 ()
  [my-common-args]                      ; Reuse in another menu
  ["Other Actions" ...])
```

### Validation before execution

```elisp
(transient-define-suffix my-create (title desc)
  (interactive
   (list (read-string "Title: ")
         (read-string "Description: ")))
  (when (string-empty-p title)
    (user-error "Title cannot be empty"))
  (my-create-thing title desc))
```

## Best Practices

### 1. Use appropriate classes
- Use `transient-switch` for boolean flags
- Use `transient-option` for value-taking arguments
- Use `transient-switches` for mutually exclusive options

### 2. Leverage levels effectively
- Put common operations at level 3-4
- Put advanced features at level 5-6
- Use level 7 for debug/experimental

### 3. Provide good descriptions
- Keep descriptions concise (fits in menu)
- Use dynamic descriptions to show state
- Use `:summary` for longer explanations

### 4. Handle state properly
- Use `:transient t` for commands that should stay in menu
- Use `:if` predicates instead of manual state checking
- Use `:inapt-if` to show unavailable options

### 5. Organize logically
- Group related items together
- Use columns for parallel choices
- Put common actions on left, advanced on right

### 6. Share history
- Use `:history-key` to share history between similar infixes
```elisp
(transient-define-argument my-author ()
  :argument "--author="
  :history-key 'my-package-author-history)
```

### 7. Provide help
- Set `:man-page` or `:info-manual` on prefix
- Use descriptive docstrings
- Implement custom `:show-help` if needed

### 8. Test interactively
- Use `C-h` while transient is active to see help
- Use `C-x l` to experiment with levels
- Use `C-x s` / `C-x C-s` to test persistence

## Common Gotchas

1. **Don't quote in transient definitions** - The macro handles it
   ```elisp
   ;; WRONG:
   ["Group" '("k" "desc" 'cmd)]

   ;; RIGHT:
   ["Group" ("k" "desc" cmd)]
   ```

2. **Use :transient t for iterative commands**
   ```elisp
   ("n" "next" my-next :transient t)    ; Can press 'n' repeatedly
   ```

3. **Remember infixes stay transient by default**
   - Don't need `:transient t` on infixes
   - They automatically use `transient--do-stay`

4. **:if vs :inapt-if**
   - `:if` - completely hide the suffix
   - `:inapt-if` - show but grayed out

5. **Accessing args in interactive**
   ```elisp
   (interactive (list (transient-args 'my-prefix)))  ; Correct
   ```

6. **Sub-prefix returns**
   ```elisp
   ("s" "sub-menu" my-sub-prefix :transient t)  ; Returns to parent
   ```

## Complete Example

```elisp
;; Custom argument
(transient-define-argument my-pkg:--author ()
  :description "Override author"
  :class 'transient-option
  :shortarg "-a"
  :argument "--author="
  :reader #'my-read-author)

;; Suffix that stays transient
(transient-define-suffix my-pkg-preview ()
  "Preview current settings."
  :transient t
  (interactive)
  (message "Args: %S" (transient-args 'my-pkg-create)))

;; Main suffix
(transient-define-suffix my-pkg-execute (args)
  "Execute with ARGS."
  (interactive (list (transient-args 'my-pkg-create)))
  (apply #'my-pkg-run args))

;; Main menu
(transient-define-prefix my-pkg-create ()
  "Create something with options."
  :man-page "my-tool"
  :value '("--verbose")

  ["Arguments"
   ("-v" "Verbose" "--verbose")
   ("-q" "Quiet" "--quiet")
   ("-n" "Dry run" "--dry-run" :level 5)
   (my-pkg:--author)]

  [["Actions"
    ("p" "Preview" my-pkg-preview)
    ("c" "Create" my-pkg-execute)]
   ["Other"
    ("q" "Quit" transient-quit)]])
```

## Reference Materials

For deeper understanding, refer to:
- **transient-reference/transient/** - Core library documentation
- **transient-reference/magit/** - Real-world usage in Magit
- **transient-reference/forge/** - Additional patterns in Forge
- **transient-reference/transient-showcase/** - Examples showcase

## Testing Transient Menus

### Why Test Transients?

Transient menus have complex state management involving:
- Infix commands that set state variables
- Suffix commands that read those states
- Pre/post-command hooks for state transitions
- Display refreshes and keybinding resolution

Testing only suffix commands (by mocking `transient-args`) skips
testing the **entire UI layer**.

### Two Testing Approaches

#### 1. Unit Testing (Mocked Args) - Fast but Limited

Test suffix commands directly by mocking `transient-args`:

```elisp
(defmacro my-test-with-transient-args (prefix args &rest body)
  "Execute BODY with transient-args mocked for PREFIX to return ARGS."
  (declare (indent 2))
  `(cl-letf (((symbol-function 'transient-args)
              (lambda (p)
                (when (eq p ,prefix)
                  ,args))))
     ,@body))

(ert-deftest my-test-suffix-execution ()
  "Test suffix command with mocked args."
  (my-test-with-transient-args 'my-prefix
      '("--title=Test" "--priority=1")
    (let ((result (call-interactively #'my-suffix-execute)))
      (should (string-match-p "Created" result)))))
```

**Pros:**
- Fast (~0.5s per test)
- Simple to write
- Good for testing suffix logic

**Cons:**
- ❌ Doesn't test infix commands
- ❌ Doesn't test transient UI
- ❌ Doesn't test user workflow
- ❌ Won't catch binding errors

#### 2. Integration Testing (execute-kbd-macro) - Slower but Complete

Test the full user interaction with keyboard macros:

```elisp
(ert-deftest my-test-full-ui-interaction ()
  "Test complete user workflow through transient UI."
  :tags '(:integration :ui)
  (my-test-with-project ()  ; Setup test environment
    ;; Invoke the transient menu
    (funcall-interactively #'my-prefix)

    ;; Set title via infix (key + input in ONE macro!)
    (execute-kbd-macro (kbd "t Test SPC Title RET"))

    ;; Set priority via infix
    (execute-kbd-macro (kbd "- p 1 RET"))

    ;; Execute the suffix
    (execute-kbd-macro (kbd "x"))

    ;; Verify results
    (let ((result (my-get-created-item)))
      (should (equal (plist-get result :title) "Test Title"))
      (should (equal (plist-get result :priority) 1)))))
```

**Pros:**
- ✅ Tests infix commands
- ✅ Tests transient UI
- ✅ Tests real user workflow
- ✅ No mocking needed
- ✅ Works in batch mode (CI-friendly!)
- ✅ Catches binding errors, UI bugs

**Cons:**
- Slower (~2.3s per test, +0.65s overhead)
- More complex to write
- Need to know key sequences

**Performance breakdown:**
- Transient infrastructure: ~0.15s
- Multiple kbd macro calls: ~0.20s
- Minibuffer interactions: ~0.20s
- Display refreshes: ~0.10s
- **Total overhead: ~0.65s per test**

### Critical Rule for execute-kbd-macro

**You MUST combine transient key + input in a SINGLE kbd macro:**

```elisp
;; ✅ CORRECT - Key + input in one macro
(execute-kbd-macro (kbd "t Bug SPC Title RET"))

;; ❌ WRONG - Splitting key and input
(execute-kbd-macro (kbd "t"))              ; Opens minibuffer
(execute-kbd-macro (kbd "Bug Title RET"))  ; Fails! Tries to invoke
                                           ; transient keys B, u, g
```

**Why?** When you split the macro, the second call doesn't go to the
minibuffer - it's interpreted as more transient commands.

### Testing Patterns

#### Pattern 1: Minimal workflow (title only)

```elisp
(ert-deftest my-test-minimal-workflow ()
  "Test simplest possible workflow."
  (my-test-setup ()
    (funcall-interactively #'my-create)
    (execute-kbd-macro (kbd "t Minimal SPC Test RET"))
    (execute-kbd-macro (kbd "x"))
    ;; Verify...
    ))
```

#### Pattern 2: Full workflow (multiple fields)

```elisp
(ert-deftest my-test-full-workflow ()
  "Test complete workflow with all fields."
  (my-test-setup ()
    (funcall-interactively #'my-create)

    ;; Set all fields
    (execute-kbd-macro (kbd "t Full SPC Test RET"))
    (execute-kbd-macro (kbd "- t feature RET"))
    (execute-kbd-macro (kbd "- p 2 RET"))
    (execute-kbd-macro (kbd "- a john@example.com RET"))

    ;; Execute
    (execute-kbd-macro (kbd "x"))

    ;; Verify all fields were set correctly
    ))
```

#### Pattern 3: Testing switches (toggle flags)

```elisp
(ert-deftest my-test-switches ()
  "Test boolean switch toggling."
  (my-test-setup ()
    (funcall-interactively #'my-prefix)

    ;; Toggle switch on
    (execute-kbd-macro (kbd "- v"))  ; --verbose

    ;; Toggle switch off
    (execute-kbd-macro (kbd "- v"))

    ;; Toggle back on
    (execute-kbd-macro (kbd "- v"))

    (execute-kbd-macro (kbd "x"))
    ;; Verify switch state...
    ))
```

#### Pattern 4: Testing transient navigation

```elisp
(ert-deftest my-test-navigation ()
  "Test moving between transient levels."
  (my-test-setup ()
    (funcall-interactively #'my-prefix)

    ;; Change transient level to show advanced options
    (execute-kbd-macro (kbd "C-x l 6 RET"))

    ;; Now advanced options should be visible
    (execute-kbd-macro (kbd "- n"))  ; Advanced option

    (execute-kbd-macro (kbd "x"))
    ;; Verify...
    ))
```

### Helper Functions

```elisp
(defun my-test-kbd-do (keys)
  "Execute keyboard macro from KEYS list.
KEYS is a list of key sequence strings that will be joined
and executed as a keyboard macro."
  (execute-kbd-macro (kbd (string-join keys " "))))

;; Usage:
(my-test-kbd-do '("t" "Title" "RET"))  ; Cleaner than raw kbd
```

### When to Use Each Approach

**Use mocked approach when:**
- Testing suffix logic in isolation
- Testing error handling
- Speed is critical (large test suite)
- Don't need to test UI interaction

**Use execute-kbd-macro approach when:**
- Testing end-to-end user workflows
- Validating infix commands work
- Testing transient state management
- Need confidence in the full UI
- Testing for a critical user path

**Recommended strategy:**
- Use mocked tests for most unit tests
- Use execute-kbd-macro for key integration tests
- At least one full UI test per transient menu

### Example: Complete Test Suite

```elisp
;;; Unit Tests (Mocked - Fast)

(ert-deftest my-create-test-parse-args ()
  "Test argument parsing."
  ;; Fast unit test, no UI
  )

(ert-deftest my-create-test-validation ()
  "Test validation logic."
  (my-test-with-transient-args 'my-create
      '("--title=")  ; Empty title
    (should-error (call-interactively #'my-create-execute)
                  :type 'user-error)))

;;; Integration Tests (Full UI - Comprehensive)

(ert-deftest my-create-test-full-ui-basic ()
  "Test basic creation workflow through UI."
  :tags '(:integration :ui)
  (my-test-setup ()
    (funcall-interactively #'my-create)
    (execute-kbd-macro (kbd "t Basic SPC Test RET"))
    (execute-kbd-macro (kbd "x"))
    ;; Verify creation...
    ))

(ert-deftest my-create-test-full-ui-all-fields ()
  "Test creation with all fields through UI."
  :tags '(:integration :ui :slow)
  (my-test-setup ()
    (funcall-interactively #'my-create)
    (execute-kbd-macro (kbd "t Full SPC Test RET"))
    (execute-kbd-macro (kbd "- t feature RET"))
    (execute-kbd-macro (kbd "- p 2 RET"))
    (execute-kbd-macro (kbd "x"))
    ;; Verify all fields...
    ))
```

### Debugging Tips

**When tests fail:**

1. **Check key bindings** - Use `C-h` in transient to see actual keys
2. **Test interactively first** - Run the transient manually
3. **Add debug messages**:
   ```elisp
   (execute-kbd-macro (kbd "t Test RET"))
   (message "After title: %S" (transient-args 'my-prefix))
   ```
4. **Check transient buffer** - Look at `*transient*` buffer state
5. **Use edebug** - Step through suffix execution

**Common issues:**

- **"Unbound suffix"** - Wrong key binding in test
- **"Wrong type argument"** - Reader function got nil/wrong type
- **Macro fails silently** - Split key + input across macros
- **Args not set** - Forgot to call `funcall-interactively` on prefix

### Testing Multiline Editor Fields

Fields that open dedicated editor buffers for multiline input (description,
notes, comments) **DO work** with `execute-kbd-macro` using the same principle
as simple fields: **combine everything in a SINGLE macro**.

**Pattern:**

```elisp
(funcall-interactively #'my-create)

;; ✅ CORRECT - Combine infix + text + commit in ONE macro
(execute-kbd-macro (kbd "- d Full SPC description SPC text C-c C-c"))
(execute-kbd-macro (kbd "- A Acceptance SPC criteria C-c C-c"))
```

**Why this works:**
1. `- d` opens the editor buffer
2. `Full SPC description SPC text` types into that buffer (it's now active)
3. `C-c C-c` commits and returns to transient menu
4. All happens in single macro execution, so context is preserved

**What DOESN'T work (split across macros):**

```elisp
;; ❌ WRONG - Split into separate macros
(execute-kbd-macro (kbd "- d"))               ; Opens editor
(execute-kbd-macro (kbd "My description"))     ; Fails! Wrong context
(execute-kbd-macro (kbd "C-c C-c"))           ; Fails! Wrong context
```

When you split into multiple `execute-kbd-macro` calls, each call starts from
the current buffer context, which may not be the editor buffer that was opened.

**Summary:**
- ✅ Simple minibuffer prompts work (combine key + input + RET)
- ✅ Switches/toggles work (just the key)
- ✅ **Multiline editor fields work** (combine key + text + C-c C-c)
- ❌ Splitting any interaction across multiple macro calls fails

### Comparison to Other Testing Approaches

**with-simulated-input package:**
- ❌ Adds complexity without clear benefits for transient testing
- ❌ Our pattern (mock transient-args + call-interactively) is simpler
- ✅ execute-kbd-macro works natively in batch mode

**Casual project approach:**
- Tests only binding structure (overrides commands with stubs)
- ❌ Doesn't test actual command execution
- ✅ Our approach tests REAL commands with REAL execution

**Result:** Our execute-kbd-macro approach gives superior coverage!

## Version History

- v1.1.0 (2025-11-19): Added comprehensive testing section with
  execute-kbd-macro patterns and performance analysis
- v1.0.0 (2025-11-06): Initial skill created from research of
  transient, Magit, Forge, and transient-showcase repositories

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.