diff --git a/gui.go b/gui.go index 847aee2..76bcb5c 100644 --- a/gui.go +++ b/gui.go @@ -73,6 +73,8 @@ type Gui struct { outputMode OutputMode stop chan struct{} blacklist []Key + testCounter int // used for testing synchronization + testNotify chan struct{} // BgColor and FgColor allow to configure the background and foreground // colors of the GUI. @@ -485,6 +487,7 @@ func (g *Gui) MainLoop() error { if err := g.flush(); err != nil { return err } + g.testCounter = 0 for { select { case ev := <-g.gEvents: @@ -505,6 +508,13 @@ func (g *Gui) MainLoop() error { if err := g.flush(); err != nil { return err } + // used during testing for synchronization + if g.testNotify != nil && g.testCounter > 0 { + for g.testCounter > 0 { + g.testCounter-- + g.testNotify <- struct{}{} + } + } } } @@ -532,6 +542,9 @@ func (g *Gui) handleEvent(ev *gocuiEvent) error { switch ev.Type { case eventKey, eventMouse: return g.onKey(ev) + case eventTime: + g.testCounter++ + return nil case eventError: return ev.Err // Not sure if this should be handled. It acts weirder when it's here diff --git a/tcell_driver.go b/tcell_driver.go index 7778db1..2fca9f4 100644 --- a/tcell_driver.go +++ b/tcell_driver.go @@ -124,6 +124,7 @@ const ( eventInterrupt eventError eventRaw + eventTime ) var ( @@ -221,6 +222,8 @@ func pollEvent() gocuiEvent { Ch: 0, Mod: mouseMod, } + case *tcell.EventTime: + return gocuiEvent{Type: eventTime} default: return gocuiEvent{Type: eventNone} } diff --git a/testing.go b/testing.go index c70de64..2fc24cd 100644 --- a/testing.go +++ b/testing.go @@ -46,11 +46,13 @@ func (g *Gui) GetTestingScreen() TestingScreen { // defer cleanup() // func (t *TestingScreen) StartGui() func() { + t.gui.testNotify = make(chan struct{}) go func() { if err := t.gui.MainLoop(); err != nil && !errors.Is(err, ErrQuit) { log.Panic(err) } }() + t.WaitSync() t.started = true @@ -76,6 +78,25 @@ func (t *TestingScreen) SendKey(key Key) { t.screen.InjectKey(tcell.Key(key), rune(key), tcell.ModNone) } +// SendsKeySync sends a key to gocui and wait until MainLoop process it. +func (t *TestingScreen) SendKeySync(key Key) { + if !t.started { + panic("TestingScreen must be started using 'StartGui' before injecting keys") + } + t.screen.InjectKey(tcell.Key(key), rune(key), tcell.ModNone) + t.WaitSync() +} + +// WaitSync sends time event to gocui and awaits notification that it was received. +// +// Notification is sent from gocui at the end of MainLoop, so after this function returns, +// user has confirmation that all the keys sent to gocui before time event were processed. +func (t *TestingScreen) WaitSync() { + ev := &tcell.EventTime{} + t.screen.PostEvent(ev) + <-t.gui.testNotify +} + // GetViewContent gets the current conent of a view from the simulated screen func (t *TestingScreen) GetViewContent(viewName string) (string, error) { if !t.started { @@ -136,21 +157,7 @@ func (t *TestingScreen) injectString(str string) { s := i * 10 e := i*10 + 10 simulationScreen.InjectKeyBytes([]byte(str[s:e])) - for i := 0; i < 10; i++ { - // Trigger GoCUI to update with new input - // Todo: Is this necessary? - t.gui.Update(func(*Gui) error { - return nil - }) - } } simulationScreen.InjectKeyBytes([]byte(str[len(str)-extra:])) - for i := 0; i < extra; i++ { - // Trigger GoCUI to update with new input - // Todo: Is this necessary? - t.gui.Update(func(*Gui) error { - return nil - }) - } } diff --git a/testing_test.go b/testing_test.go index 837fa9f..3d3a8f5 100644 --- a/testing_test.go +++ b/testing_test.go @@ -7,8 +7,8 @@ package gocui import ( "errors" "fmt" - "log" "strings" + "sync" "testing" "time" ) @@ -22,7 +22,7 @@ func TestTestingScreenReturnsCorrectContent(t *testing.T) { // Create a view specifying the "OutputSimulator" mode g, err := NewGui(OutputSimulator, true) if err != nil { - log.Panicln(err) + t.Error(err) } g.SetManagerFunc(func(g *Gui) error { maxX, maxY := g.Size() @@ -48,7 +48,7 @@ func TestTestingScreenReturnsCorrectContent(t *testing.T) { return nil } if err := g.SetKeybinding("", KeyCtrlC, ModNone, exampleBindingToTest); err != nil { - log.Panicln(err) + t.Error(err) } // Create a test screen and start gocui @@ -56,25 +56,222 @@ func TestTestingScreenReturnsCorrectContent(t *testing.T) { cleanup := testingScreen.StartGui() defer cleanup() + // NOTE: This sequence can be replaced with `testingScreen.SendKeySync(KeyCtrlC)` (we keep it for covering the use case) // Send a key to gocui testingScreen.SendKey(KeyCtrlC) - // Wait for key to be processed - <-time.After(time.Millisecond * 50) + testingScreen.WaitSync() // Test that the keybinding fired and set "didCallCTRLC" to true if !didCallCTRLC { t.Error("Expect the simulator to invoke the key handler for CTRLC") } - // Get the content from the testing screen - actualContent, err := testingScreen.GetViewContent(viewName) + // check view content + assertView(t, testingScreen, viewName, expectedViewContent) +} + +func TestTestingScreenMultipleKeys(t *testing.T) { + // Track what happened in the view, we'll assert on these + didCallCTRLC := false + expectedViewContent := "Hello world!" + expectedViewContent1 := "Hello World!" + expectedViewContent2 := "HELLO WORLD!" + expectedViewContent3 := "Hello lord!!" + viewName := "testView1" + + // Create a view specifying the "OutputSimulator" mode + g, err := NewGui(OutputSimulator, true) if err != nil { t.Error(err) } + g.SetManagerFunc(func(g *Gui) error { + maxX, maxY := g.Size() + if v, err := g.SetView(viewName, maxX/2-7, maxY/2, maxX/2+7, maxY/2+2, 0); err != nil { + if !errors.Is(err, ErrUnknownView) { + return err + } + + if _, err := g.SetCurrentView(viewName); err != nil { + return err + } + + // Have the view draw "Hello world!" + fmt.Fprintln(v, expectedViewContent) + } + + return nil + }) + + // Create a key binding which sets "didCallCTRLC" when triggered + exampleBindingToTest := func(g *Gui, v *View) error { + didCallCTRLC = true + return nil + } + if err := g.SetKeybinding("", KeyCtrlC, ModNone, exampleBindingToTest); err != nil { + t.Error(err) + } + + if err := g.SetKeybinding("", KeyF1, ModNone, func(g *Gui, v *View) error { + v.Clear() + fmt.Fprintln(v, expectedViewContent1) + return nil + }); err != nil { + t.Error(err) + } + if err := g.SetKeybinding("", KeyF2, ModNone, func(g *Gui, v *View) error { + v.Clear() + <-time.After(time.Millisecond * 100) + fmt.Fprintln(v, expectedViewContent2) + return nil + }); err != nil { + t.Error(err) + } + if err := g.SetKeybinding("", KeyF3, ModNone, func(g *Gui, v *View) error { + v.Clear() + fmt.Fprintln(v, expectedViewContent3) + return nil + }); err != nil { + t.Error(err) + } + + // Create a test screen and start gocui + testingScreen := g.GetTestingScreen() + cleanup := testingScreen.StartGui() + defer cleanup() + + // check view content + assertView(t, testingScreen, viewName, expectedViewContent) + + // Send a key to gocui + testingScreen.SendKeySync(KeyCtrlC) + + // Test that the keybinding fired and set "didCallCTRLC" to true + if !didCallCTRLC { + t.Error("Expect the simulator to invoke the key handler for CTRLC") + } - // Test that it contains the "Hello World!" we thought the view should draw - if strings.TrimSpace(actualContent) != expectedViewContent { - t.Error(fmt.Printf("Expected view content to be: %q got: %q", expectedViewContent, actualContent)) + tests := []struct { + key Key + content string + }{ + {KeyF1, expectedViewContent1}, + {KeyF2, expectedViewContent2}, + {KeyF3, expectedViewContent3}, + } + for _, key := range tests { + // Send a key to gocui + testingScreen.SendKeySync(key.key) + // check view content + assertView(t, testingScreen, viewName, key.content) + } +} + +func TestTestingScreenParallelKeys(t *testing.T) { + // Track what happened in the view, we'll assert on these + didCallCTRLC := false + didCallF1 := false + didCallF2 := false + didCallF3 := false + expectedViewContent := "Hello world!" + viewName := "testView1" + + // Create a view specifying the "OutputSimulator" mode + g, err := NewGui(OutputSimulator, true) + if err != nil { + t.Error(err) + } + g.SetManagerFunc(func(g *Gui) error { + maxX, maxY := g.Size() + if v, err := g.SetView(viewName, maxX/2-7, maxY/2, maxX/2+7, maxY/2+2, 0); err != nil { + if !errors.Is(err, ErrUnknownView) { + return err + } + + if _, err := g.SetCurrentView(viewName); err != nil { + return err + } + + // Have the view draw "Hello world!" + fmt.Fprintln(v, expectedViewContent) + } + + return nil + }) + + // Create a key bindings + if err := g.SetKeybinding("", KeyCtrlC, ModNone, func(g *Gui, v *View) error { + didCallCTRLC = true + return nil + }); err != nil { + t.Error(err) + } + + if err := g.SetKeybinding("", KeyF1, ModNone, func(g *Gui, v *View) error { + didCallF1 = true + return nil + }); err != nil { + t.Error(err) + } + if err := g.SetKeybinding("", KeyF2, ModNone, func(g *Gui, v *View) error { + didCallF2 = true + return nil + }); err != nil { + t.Error(err) + } + if err := g.SetKeybinding("", KeyF3, ModNone, func(g *Gui, v *View) error { + didCallF3 = true + return nil + }); err != nil { + t.Error(err) + } + + // Create a test screen and start gocui + testingScreen := g.GetTestingScreen() + cleanup := testingScreen.StartGui() + defer cleanup() + + // check view content + assertView(t, testingScreen, viewName, expectedViewContent) + + // Send a key to gocui + testingScreen.SendKeySync(KeyCtrlC) + var wg sync.WaitGroup + wg.Add(3) + go func() { + testingScreen.SendKeySync(KeyF1) + wg.Done() + }() + go func() { + testingScreen.SendKeySync(KeyF2) + wg.Done() + }() + go func() { + testingScreen.SendKeySync(KeyF3) + wg.Done() + }() + + wg.Wait() + + // Test that the keybinding fired + if !didCallCTRLC { + t.Error("Expect the simulator to invoke the key handler for CTRLC") + } + if !didCallF1 || !didCallF2 || !didCallF3 { + t.Error("Expect the simulator to invoke the key handler for F1, F2 and F3") + } +} + +// assertView checks if view contains provided content. +func assertView(t *testing.T, ts TestingScreen, viewName, content string) { + t.Helper() + // Get the content from the testing screen + if actualContent, err := ts.GetViewContent(viewName); err != nil { + t.Error(err) + } else { + // Test that it contains the "Hello World!" we thought the view should draw + if strings.TrimSpace(actualContent) != content { + t.Error(fmt.Printf("Expected view content to be: %q got: %q", content, actualContent)) + } } }