editor_test.go_ raw

   1  // TODO: this needs to be updated
   2  // SPDX-License-Identifier: Unlicense OR MIT
   3  
   4  package gui
   5  
   6  import (
   7  	"fmt"
   8  	"image"
   9  	"math/rand"
  10  	"reflect"
  11  	"testing"
  12  	"testing/quick"
  13  	"unicode"
  14  
  15  	"github.com/p9c/gio/f32"
  16  	"github.com/p9c/gio/font/gofont"
  17  	"github.com/p9c/gio/io/event"
  18  	"github.com/p9c/gio/io/key"
  19  	"github.com/p9c/gio/layout"
  20  	"github.com/p9c/gio/op"
  21  	"github.com/p9c/gio/text"
  22  	"github.com/p9c/gio/unit"
  23  )
  24  
  25  func TestEditor(t *testing.T) {
  26  	e := new(Editor)
  27  	gtx := layout.Context{
  28  		Ops:         new(op.Ops),
  29  		Constraints: layout.Exact(image.Pt(100, 100)),
  30  	}
  31  	cache := text.NewCache(gofont.Collection())
  32  	fontSize := unit.Px(10)
  33  	font := text.Font{}
  34  
  35  	e.SetText("æbc\naøå•")
  36  	e.Layout(gtx, cache, font, fontSize)
  37  	assertCaret(t, e, 0, 0, 0)
  38  	e.moveEnd()
  39  	assertCaret(t, e, 0, 3, len("æbc"))
  40  	e.Move(+1)
  41  	assertCaret(t, e, 1, 0, len("æbc\n"))
  42  	e.Move(-1)
  43  	assertCaret(t, e, 0, 3, len("æbc"))
  44  	e.moveLines(+1)
  45  	assertCaret(t, e, 1, 3, len("æbc\naøå"))
  46  	e.moveEnd()
  47  	assertCaret(t, e, 1, 4, len("æbc\naøå•"))
  48  	e.Move(+1)
  49  	assertCaret(t, e, 1, 4, len("æbc\naøå•"))
  50  
  51  	// Ensure that password masking does not affect caret behavior
  52  	e.Move(-3)
  53  	assertCaret(t, e, 1, 1, len("æbc\na"))
  54  	e.mask = '*'
  55  	e.Layout(gtx, cache, font, fontSize)
  56  	assertCaret(t, e, 1, 1, len("æbc\na"))
  57  	e.Move(-3)
  58  	assertCaret(t, e, 0, 2, len("æb"))
  59  	e.mask = '\U0001F92B'
  60  	e.Layout(gtx, cache, font, fontSize)
  61  	e.moveEnd()
  62  	assertCaret(t, e, 0, 3, len("æbc"))
  63  
  64  	// When a password mask is applied, it should replace all visible glyphs
  65  	for i, line := range e.lines {
  66  		for j, glyph := range line.Layout {
  67  			if glyph.Rune != e.mask && !unicode.IsSpace(glyph.Rune) {
  68  				t.Errorf("glyph at (%d, %d) is unmasked rune %d", i, j, glyph.Rune)
  69  			}
  70  		}
  71  	}
  72  }
  73  
  74  func TestEditorDimensions(t *testing.T) {
  75  	e := new(Editor)
  76  	tq := &testQueue{
  77  		events: []event.Event{
  78  			key.EditEvent{Text: "A"},
  79  		},
  80  	}
  81  	gtx := layout.Context{
  82  		Ops:         new(op.Ops),
  83  		Constraints: layout.Constraints{Max: image.Pt(100, 100)},
  84  		Queue:       tq,
  85  	}
  86  	cache := text.NewCache(gofont.Collection())
  87  	fontSize := unit.Px(10)
  88  	font := text.Font{}
  89  	dims := e.Layout(gtx, cache, font, fontSize)
  90  	if dims.Size.X == 0 {
  91  		t.Errorf("EditEvent was not reflected in _editor width")
  92  	}
  93  }
  94  
  95  type testQueue struct {
  96  	events []event.Event
  97  }
  98  
  99  func (q *testQueue) Events(_ event.Tag) []event.Event {
 100  	return q.events
 101  }
 102  
 103  // assertCaret asserts that the editor caret is at a particular line
 104  // and column, and that the byte position matches as well.
 105  func assertCaret(t *testing.T, e *Editor, line, col, bytes int) {
 106  	t.Helper()
 107  	gotLine, gotCol := e.CaretPos()
 108  	if gotLine != line || gotCol != col {
 109  		t.Errorf("caret at (%d, %d), expected (%d, %d)", gotLine, gotCol, line, col)
 110  	}
 111  	if bytes != e.rr.caret {
 112  		t.Errorf("caret at buffer position %d, expected %d", e.rr.caret, bytes)
 113  	}
 114  }
 115  
 116  type editMutation int
 117  
 118  const (
 119  	setText editMutation = iota
 120  	moveRune
 121  	moveLine
 122  	movePage
 123  	moveStart
 124  	moveEnd
 125  	moveCoord
 126  	moveWord
 127  	deleteWord
 128  	moveLast // Mark end; never generated.
 129  )
 130  
 131  func TestEditorCaretConsistency(t *testing.T) {
 132  	gtx := layout.Context{
 133  		Ops:         new(op.Ops),
 134  		Constraints: layout.Exact(image.Pt(100, 100)),
 135  	}
 136  	cache := text.NewCache(gofont.Collection())
 137  	fontSize := unit.Px(10)
 138  	font := text.Font{}
 139  	for _, a := range []text.Alignment{text.Start, text.Middle, text.End} {
 140  		e := &Editor{
 141  			alignment: a,
 142  		}
 143  		e.Layout(gtx, cache, font, fontSize)
 144  
 145  		consistent := func() (e error) {
 146  			t.Helper()
 147  			gotLine, gotCol := e.CaretPos()
 148  			gotCoords := e.CaretCoords()
 149  			wantLine, wantCol, wantX, wantY := e.layoutCaret()
 150  			wantCoords := f32.Pt(float32(wantX)/64, float32(wantY))
 151  			if wantLine == gotLine && wantCol == gotCol && gotCoords == wantCoords {
 152  				return nil
 153  			}
 154  			return fmt.Errorf("caret (%d,%d) pos %s, want (%d,%d) pos %s", gotLine, gotCol, gotCoords, wantLine, wantCol, wantCoords)
 155  		}
 156  		if e := consistent(); dbg.Chk(e) {
 157  			t.Errorf("initial editor inconsistency (alignment %s): %v", a, err)
 158  		}
 159  
 160  		move := func(mutation editMutation, str string, distance int8, x, y uint16) bool {
 161  			switch mutation {
 162  			case setText:
 163  				e.SetText(str)
 164  				e.Layout(gtx, cache, font, fontSize)
 165  			case moveRune:
 166  				e.Move(int(distance))
 167  			case moveLine:
 168  				e.moveLines(int(distance))
 169  			case movePage:
 170  				e.movePages(int(distance))
 171  			case moveStart:
 172  				e.moveStart()
 173  			case moveEnd:
 174  				e.moveEnd()
 175  			case moveCoord:
 176  				e.moveCoord(image.Pt(int(x), int(y)))
 177  			case moveWord:
 178  				e.moveWord(int(distance))
 179  			case deleteWord:
 180  				e.deleteWord(int(distance))
 181  			default:
 182  				return false
 183  			}
 184  			if e := consistent(); dbg.Chk(e) {
 185  				t.				return false
 186  			}
 187  			return true
 188  		}
 189  		if e := quick.Check(move, nil); dbg.Chk(e) {
 190  			t.Errorf("editor inconsistency (alignment %s): %v", a, err)
 191  		}
 192  	}
 193  }
 194  
 195  func TestEditorMoveWord(t *testing.T) {
 196  	type Test struct {
 197  		Text  string
 198  		Start int
 199  		Skip  int
 200  		Want  int
 201  	}
 202  	tests := []Test{
 203  		{"", 0, 0, 0},
 204  		{"", 0, -1, 0},
 205  		{"", 0, 1, 0},
 206  		{"hello", 0, -1, 0},
 207  		{"hello", 0, 1, 5},
 208  		{"hello world", 3, 1, 5},
 209  		{"hello world", 3, -1, 0},
 210  		{"hello world", 8, -1, 6},
 211  		{"hello world", 8, 1, 11},
 212  		{"hello    world", 3, 1, 5},
 213  		{"hello    world", 3, 2, 14},
 214  		{"hello    world", 8, 1, 14},
 215  		{"hello    world", 8, -1, 0},
 216  		{"hello brave new world", 0, 3, 15},
 217  	}
 218  	setup := func(t string) *Editor {
 219  		e := new(Editor)
 220  		gtx := layout.Context{
 221  			Ops:         new(op.Ops),
 222  			Constraints: layout.Exact(image.Pt(100, 100)),
 223  		}
 224  		cache := text.NewCache(gofont.Collection())
 225  		fontSize := unit.Px(10)
 226  		font := text.Font{}
 227  		e.SetText(t)
 228  		e.Layout(gtx, cache, font, fontSize)
 229  		return e
 230  	}
 231  	for ii, tt := range tests {
 232  		e := setup(tt.Text)
 233  		e.Move(tt.Start)
 234  		e.moveWord(tt.Skip)
 235  		if e.rr.caret != tt.Want {
 236  			t.Fatalf("[%d] moveWord: bad caret position: got %d, want %d", ii, e.rr.caret, tt.Want)
 237  		}
 238  	}
 239  }
 240  
 241  func TestEditorDeleteWord(t *testing.T) {
 242  	type Test struct {
 243  		Text   string
 244  		Start  int
 245  		Delete int
 246  
 247  		Want   int
 248  		Result string
 249  	}
 250  	tests := []Test{
 251  		{"", 0, 0, 0, ""},
 252  		{"", 0, -1, 0, ""},
 253  		{"", 0, 1, 0, ""},
 254  		{"hello", 0, -1, 0, "hello"},
 255  		{"hello", 0, 1, 0, ""},
 256  		{"hello world", 3, 1, 3, "hel world"},
 257  		{"hello world", 3, -1, 0, "lo world"},
 258  		{"hello world", 8, -1, 6, "hello rld"},
 259  		{"hello world", 8, 1, 8, "hello wo"},
 260  		{"hello    world", 3, 1, 3, "hel    world"},
 261  		{"hello    world", 3, 2, 3, "helworld"},
 262  		{"hello    world", 8, 1, 8, "hello   "},
 263  		{"hello    world", 8, -1, 5, "hello world"},
 264  		{"hello brave new world", 0, 3, 0, " new world"},
 265  	}
 266  	setup := func(t string) *Editor {
 267  		e := new(Editor)
 268  		gtx := layout.Context{
 269  			Ops:         new(op.Ops),
 270  			Constraints: layout.Exact(image.Pt(100, 100)),
 271  		}
 272  		cache := text.NewCache(gofont.Collection())
 273  		fontSize := unit.Px(10)
 274  		font := text.Font{}
 275  		e.SetText(t)
 276  		e.Layout(gtx, cache, font, fontSize)
 277  		return e
 278  	}
 279  	for ii, tt := range tests {
 280  		e := setup(tt.Text)
 281  		e.Move(tt.Start)
 282  		e.deleteWord(tt.Delete)
 283  		if e.rr.caret != tt.Want {
 284  			t.Fatalf("[%d] deleteWord: bad caret position: got %d, want %d", ii, e.rr.caret, tt.Want)
 285  		}
 286  		if e.Text() != tt.Result {
 287  			t.Fatalf("[%d] deleteWord: invalid result: got %q, want %q", ii, e.Text(), tt.Result)
 288  		}
 289  	}
 290  }
 291  
 292  func TestEditorNoLayout(t *testing.T) {
 293  	var e Editor
 294  	e.SetText("hi!\n")
 295  	e.Move(1)
 296  }
 297  
 298  // Generate generates a value of itself, for testing/quick.
 299  func (editMutation) Generate(rand *rand.Rand, size int) reflect.Value {
 300  	t := editMutation(rand.Intn(int(moveLast)))
 301  	return reflect.ValueOf(t)
 302  }
 303