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