Add support for pasting text in integration tests

The commit message panel handles pasted text differently from typed
text (see ba6cfc1f85), but integration tests couldn't simulate a
paste, so this had no tests. Let tests replay the events that a
terminal sends for a bracketed paste: the start of the paste, a key
press for each character, and the end of the paste.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Stefan Haller
2026-11-12 10:22:50 +01:00
co-authored by Claude Opus 5.5
parent b65298c3b1
commit dec9e49162
8 changed files with 80 additions and 0 deletions
+9
View File
@@ -119,6 +119,7 @@ type replayedEvents struct {
Resizes chan *TcellResizeEventWrapper
MouseEvents chan *TcellMouseEventWrapper
FocusEvents chan *TcellFocusEventWrapper
PasteEvents chan *TcellPasteEventWrapper
}
type RecordingConfig struct {
@@ -305,6 +306,7 @@ func NewGui(opts NewGuiOpts) (*Gui, error) {
Resizes: make(chan *TcellResizeEventWrapper),
MouseEvents: make(chan *TcellMouseEventWrapper),
FocusEvents: make(chan *TcellFocusEventWrapper),
PasteEvents: make(chan *TcellPasteEventWrapper),
}
}
@@ -384,6 +386,13 @@ func (g *Gui) ReplayFocusEvent(ev *TcellFocusEventWrapper) {
g.replayedEvents.FocusEvents <- ev
}
// ReplayPasteEvent is like ReplayKeyEvent, but for the events that mark the
// start and the end of a paste.
func (g *Gui) ReplayPasteEvent(ev *TcellPasteEventWrapper) {
ev.task = g.NewTask()
g.replayedEvents.PasteEvents <- ev
}
// Busy reports whether any foreground work is in flight, ignoring the event
// currently being processed on the main goroutine (see currentTask). Background
// routines (auto-fetch etc.) don't count. It's used to decide whether it's safe
+19
View File
@@ -302,6 +302,22 @@ func (wrapper TcellFocusEventWrapper) toTcellEvent() tcell.Event {
return tcell.NewEventFocus(wrapper.Focused)
}
type TcellPasteEventWrapper struct {
Start bool
task Task // see GocuiEvent.task
}
func NewTcellPasteEventWrapper(event *tcell.EventPaste) *TcellPasteEventWrapper {
return &TcellPasteEventWrapper{
Start: event.Start(),
}
}
func (wrapper TcellPasteEventWrapper) toTcellEvent() tcell.Event {
return tcell.NewEventPaste(wrapper.Start)
}
// pollEvent get tcell.Event and transform it into gocuiEvent
func (g *Gui) pollEvent() GocuiEvent {
var tev tcell.Event
@@ -319,6 +335,9 @@ func (g *Gui) pollEvent() GocuiEvent {
case ev := <-g.replayedEvents.FocusEvents:
tev = ev.toTcellEvent()
task = ev.task
case ev := <-g.replayedEvents.PasteEvents:
tev = ev.toTcellEvent()
task = ev.task
}
} else {
tev = <-Screen.EventQ()
+25
View File
@@ -138,6 +138,31 @@ func (self *GuiDriver) FocusIn() {
self.waitTillIdle()
}
// Paste sends the events that a terminal with bracketed paste mode sends: the
// start of the paste, a key press for each character, and the end of the paste.
func (self *GuiDriver) Paste(text string) {
self.CheckAllToastsAcknowledged()
self.gui.g.ReplayPasteEvent(gocui.NewTcellPasteEventWrapper(tcell.NewEventPaste(true)))
for _, char := range text {
self.gui.g.ReplayKeyEvent(gocui.NewTcellKeyEventWrapper(keyEventForPastedCharacter(char)))
}
self.gui.g.ReplayPasteEvent(gocui.NewTcellPasteEventWrapper(tcell.NewEventPaste(false)))
self.waitTillIdle()
}
func keyEventForPastedCharacter(char rune) *tcell.EventKey {
switch char {
case '\n':
return tcell.NewEventKey(tcell.KeyEnter, "", tcell.ModNone)
case '\t':
return tcell.NewEventKey(tcell.KeyTab, "", tcell.ModNone)
default:
return tcell.NewEventKey(tcell.KeyRune, string(char), tcell.ModNone)
}
}
func (self *GuiDriver) FocusInAndClick(x, y int) {
self.CheckAllToastsAcknowledged()
@@ -21,6 +21,14 @@ func (self *CommitDescriptionPanelDriver) Type(value string) *CommitDescriptionP
return self
}
// Paste the text the way a terminal does it, as opposed to pasting it with the
// commit menu
func (self *CommitDescriptionPanelDriver) Paste(text string) *CommitDescriptionPanelDriver {
self.t.paste(text)
return self
}
func (self *CommitDescriptionPanelDriver) SwitchToSummary() *CommitMessagePanelDriver {
self.getViewDriver().PressTab()
return &CommitMessagePanelDriver{t: self.t}
@@ -33,6 +33,14 @@ func (self *CommitMessagePanelDriver) Type(value string) *CommitMessagePanelDriv
return self
}
// Paste the text the way a terminal does it, as opposed to pasting it with the
// commit menu
func (self *CommitMessagePanelDriver) Paste(text string) *CommitMessagePanelDriver {
self.t.paste(text)
return self
}
func (self *CommitMessagePanelDriver) SwitchToDescription() *CommitDescriptionPanelDriver {
self.getViewDriver().PressTab()
return &CommitDescriptionPanelDriver{t: self.t}
@@ -136,6 +136,12 @@ func (self *TestDriver) FocusIn() {
self.Wait(self.inputDelay)
}
func (self *TestDriver) paste(text string) {
self.SetCaption(fmt.Sprintf("Pasting %q", text))
self.gui.Paste(text)
self.Wait(self.inputDelay)
}
func (self *TestDriver) focusInAndClick(x, y int) {
self.SetCaption(fmt.Sprintf("Focusing window and clicking %d, %d", x, y))
self.gui.FocusInAndClick(x, y)
+3
View File
@@ -84,6 +84,9 @@ func (self *fakeGuiDriver) OnUIThreadAndWait(f func()) {
func (self *fakeGuiDriver) FocusIn() {
}
func (self *fakeGuiDriver) Paste(text string) {
}
func (self *fakeGuiDriver) FocusInAndClick(x, y int) {
self.clickedCoordinates = append(self.clickedCoordinates, coordinate{x: x, y: y})
}
+2
View File
@@ -47,6 +47,8 @@ type GuiDriver interface {
// Simulate the terminal window regaining focus (which triggers a reload of
// changed config files)
FocusIn()
// Simulate pasting text in a terminal that supports bracketed paste
Paste(string)
// Simulate a terminal dispatching focus-in immediately followed by a click,
// without waiting for the focus refresh to finish in between.
FocusInAndClick(int, int)