Files
gio/widget/editor_test.go
T
tainted-bit 5c0f190849 widget: add optional password masking to Editor
This change adds optional password masking to the Editor. To enable
this feature, set the new Mask field to a non-zero rune. Every rune
in the Editor's contents will be replaced by the mask rune in the
visual display, except for newlines. The actual contents of the
editor can still be accessed with Len, Text, and SetText.

Fixes gio#80

Signed-off-by: tainted-bit <sourcehut@taintedbit.com>
2020-06-21 10:54:41 +02:00

163 lines
4.0 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
package widget
import (
"fmt"
"image"
"math/rand"
"reflect"
"testing"
"testing/quick"
"unicode"
"gioui.org/f32"
"gioui.org/font/gofont"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/text"
"gioui.org/unit"
)
func TestEditor(t *testing.T) {
e := new(Editor)
gtx := layout.Context{
Ops: new(op.Ops),
Constraints: layout.Exact(image.Pt(100, 100)),
}
cache := text.NewCache(gofont.Collection())
fontSize := unit.Px(10)
font := text.Font{}
e.SetText("æbc\naøå•")
e.Layout(gtx, cache, font, fontSize)
assertCaret(t, e, 0, 0, 0)
e.moveEnd()
assertCaret(t, e, 0, 3, len("æbc"))
e.Move(+1)
assertCaret(t, e, 1, 0, len("æbc\n"))
e.Move(-1)
assertCaret(t, e, 0, 3, len("æbc"))
e.moveLines(+1)
assertCaret(t, e, 1, 3, len("æbc\naøå"))
e.moveEnd()
assertCaret(t, e, 1, 4, len("æbc\naøå•"))
e.Move(+1)
assertCaret(t, e, 1, 4, len("æbc\naøå•"))
// Ensure that password masking does not affect caret behavior
e.Move(-3)
assertCaret(t, e, 1, 1, len("æbc\na"))
e.Mask = '*'
e.Layout(gtx, cache, font, fontSize)
assertCaret(t, e, 1, 1, len("æbc\na"))
e.Move(-3)
assertCaret(t, e, 0, 2, len("æb"))
e.Mask = '\U0001F92B'
e.Layout(gtx, cache, font, fontSize)
e.moveEnd()
assertCaret(t, e, 0, 3, len("æbc"))
// When a password mask is applied, it should replace all visible glyphs
for i, line := range e.lines {
for j, glyph := range line.Layout {
if glyph.Rune != e.Mask && !unicode.IsSpace(glyph.Rune) {
t.Errorf("glyph at (%d, %d) is unmasked rune %d", i, j, glyph.Rune)
}
}
}
}
// assertCaret asserts that the editor caret is at a particular line
// and column, and that the byte position matches as well.
func assertCaret(t *testing.T, e *Editor, line, col, bytes int) {
t.Helper()
gotLine, gotCol := e.CaretPos()
if gotLine != line || gotCol != col {
t.Errorf("caret at (%d, %d), expected (%d, %d)", gotLine, gotCol, line, col)
}
if bytes != e.rr.caret {
t.Errorf("caret at buffer position %d, expected %d", e.rr.caret, bytes)
}
}
type editMutation int
const (
setText editMutation = iota
moveRune
moveLine
movePage
moveStart
moveEnd
moveCoord
moveLast // Mark end; never generated.
)
func TestEditorCaretConsistency(t *testing.T) {
gtx := layout.Context{
Ops: new(op.Ops),
Constraints: layout.Exact(image.Pt(100, 100)),
}
cache := text.NewCache(gofont.Collection())
fontSize := unit.Px(10)
font := text.Font{}
for _, a := range []text.Alignment{text.Start, text.Middle, text.End} {
e := &Editor{
Alignment: a,
}
e.Layout(gtx, cache, font, fontSize)
consistent := func() error {
t.Helper()
gotLine, gotCol := e.CaretPos()
gotCoords := e.CaretCoords()
wantLine, wantCol, wantX, wantY := e.layoutCaret()
wantCoords := f32.Pt(float32(wantX)/64, float32(wantY))
if wantLine == gotLine && wantCol == gotCol && gotCoords == wantCoords {
return nil
}
return fmt.Errorf("caret (%d,%d) pos %s, want (%d,%d) pos %s", gotLine, gotCol, gotCoords, wantLine, wantCol, wantCoords)
}
if err := consistent(); err != nil {
t.Errorf("initial editor inconsistency (alignment %s): %v", a, err)
}
move := func(mutation editMutation, str string, distance int8, x, y uint16) bool {
switch mutation {
case setText:
e.SetText(str)
e.Layout(gtx, cache, font, fontSize)
case moveRune:
e.Move(int(distance))
case moveLine:
e.moveLines(int(distance))
case movePage:
e.movePages(int(distance))
case moveStart:
e.moveStart()
case moveEnd:
e.moveEnd()
case moveCoord:
e.moveCoord(image.Pt(int(x), int(y)))
default:
return false
}
if err := consistent(); err != nil {
t.Error(err)
return false
}
return true
}
if err := quick.Check(move, nil); err != nil {
t.Errorf("editor inconsistency (alignment %s): %v", a, err)
}
}
}
// Generate generates a value of itself, for testing/quick.
func (editMutation) Generate(rand *rand.Rand, size int) reflect.Value {
t := editMutation(rand.Intn(int(moveLast)))
return reflect.ValueOf(t)
}