372 lines
13 KiB
Go
372 lines
13 KiB
Go
package test
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/driver/desktop"
|
|
"fyne.io/fyne/v2/internal/cache"
|
|
"fyne.io/fyne/v2/internal/driver"
|
|
"fyne.io/fyne/v2/internal/painter/software"
|
|
"fyne.io/fyne/v2/internal/test"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// AssertCanvasTappableAt asserts that the canvas is tappable at the given position.
|
|
func AssertCanvasTappableAt(t *testing.T, c fyne.Canvas, pos fyne.Position) bool {
|
|
if o, _ := findTappable(c, pos); o == nil {
|
|
t.Errorf("No tappable found at %#v", pos)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// AssertObjectRendersToImage asserts that the given `CanvasObject` renders the same image as the one stored in the master file.
|
|
// The theme used is the standard test theme which may look different to how it shows on your device.
|
|
// The master filename is relative to the `testdata` directory which is relative to the test.
|
|
// The test `t` fails if the given image is not equal to the loaded master image.
|
|
// In this case the given image is written into a file in `testdata/failed/<masterFilename>` (relative to the test).
|
|
// This path is also reported, thus the file can be used as new master.
|
|
//
|
|
// Since 2.3
|
|
func AssertObjectRendersToImage(t *testing.T, masterFilename string, o fyne.CanvasObject, msgAndArgs ...interface{}) bool {
|
|
c := NewCanvasWithPainter(software.NewPainter())
|
|
c.SetPadded(false)
|
|
size := o.MinSize().Max(o.Size())
|
|
c.SetContent(o)
|
|
c.Resize(size) // ensure we are large enough for current size
|
|
|
|
return AssertRendersToImage(t, masterFilename, c, msgAndArgs...)
|
|
}
|
|
|
|
// AssertObjectRendersToMarkup asserts that the given `CanvasObject` renders the same markup as the one stored in the master file.
|
|
// The master filename is relative to the `testdata` directory which is relative to the test.
|
|
// The test `t` fails if the rendered markup is not equal to the loaded master markup.
|
|
// In this case the rendered markup is written into a file in `testdata/failed/<masterFilename>` (relative to the test).
|
|
// This path is also reported, thus the file can be used as new master.
|
|
//
|
|
// Be aware, that the indentation has to use tab characters ('\t') instead of spaces.
|
|
// Every element starts on a new line indented one more than its parent.
|
|
// Closing elements stand on their own line, too, using the same indentation as the opening element.
|
|
// The only exception to this are text elements which do not contain line breaks unless the text includes them.
|
|
//
|
|
// Since 2.3
|
|
func AssertObjectRendersToMarkup(t *testing.T, masterFilename string, o fyne.CanvasObject, msgAndArgs ...interface{}) bool {
|
|
c := NewCanvas()
|
|
c.SetPadded(false)
|
|
size := o.MinSize().Max(o.Size())
|
|
c.SetContent(o)
|
|
c.Resize(size) // ensure we are large enough for current size
|
|
|
|
return AssertRendersToMarkup(t, masterFilename, c, msgAndArgs...)
|
|
}
|
|
|
|
// AssertImageMatches asserts that the given image is the same as the one stored in the master file.
|
|
// The master filename is relative to the `testdata` directory which is relative to the test.
|
|
// The test `t` fails if the given image is not equal to the loaded master image.
|
|
// In this case the given image is written into a file in `testdata/failed/<masterFilename>` (relative to the test).
|
|
// This path is also reported, thus the file can be used as new master.
|
|
func AssertImageMatches(t *testing.T, masterFilename string, img image.Image, msgAndArgs ...interface{}) bool {
|
|
return test.AssertImageMatches(t, masterFilename, img, msgAndArgs...)
|
|
}
|
|
|
|
// AssertRendersToImage asserts that the given canvas renders the same image as the one stored in the master file.
|
|
// The master filename is relative to the `testdata` directory which is relative to the test.
|
|
// The test `t` fails if the given image is not equal to the loaded master image.
|
|
// In this case the given image is written into a file in `testdata/failed/<masterFilename>` (relative to the test).
|
|
// This path is also reported, thus the file can be used as new master.
|
|
//
|
|
// Since 2.3
|
|
func AssertRendersToImage(t *testing.T, masterFilename string, c fyne.Canvas, msgAndArgs ...interface{}) bool {
|
|
return test.AssertImageMatches(t, masterFilename, c.Capture(), msgAndArgs...)
|
|
}
|
|
|
|
// AssertRendersToMarkup asserts that the given canvas renders the same markup as the one stored in the master file.
|
|
// The master filename is relative to the `testdata` directory which is relative to the test.
|
|
// The test `t` fails if the rendered markup is not equal to the loaded master markup.
|
|
// In this case the rendered markup is written into a file in `testdata/failed/<masterFilename>` (relative to the test).
|
|
// This path is also reported, thus the file can be used as new master.
|
|
//
|
|
// Be aware, that the indentation has to use tab characters ('\t') instead of spaces.
|
|
// Every element starts on a new line indented one more than its parent.
|
|
// Closing elements stand on their own line, too, using the same indentation as the opening element.
|
|
// The only exception to this are text elements which do not contain line breaks unless the text includes them.
|
|
//
|
|
// Since: 2.0
|
|
func AssertRendersToMarkup(t *testing.T, masterFilename string, c fyne.Canvas, msgAndArgs ...interface{}) bool {
|
|
wd, err := os.Getwd()
|
|
require.NoError(t, err)
|
|
|
|
got := snapshot(c)
|
|
masterPath := filepath.Join(wd, "testdata", masterFilename)
|
|
failedPath := filepath.Join(wd, "testdata/failed", masterFilename)
|
|
_, err = os.Stat(masterPath)
|
|
if os.IsNotExist(err) {
|
|
require.NoError(t, writeMarkup(failedPath, got))
|
|
t.Errorf("Master not found at %s. Markup written to %s might be used as master.", masterPath, failedPath)
|
|
return false
|
|
}
|
|
|
|
raw, err := os.ReadFile(masterPath)
|
|
require.NoError(t, err)
|
|
master := strings.ReplaceAll(string(raw), "\r", "")
|
|
|
|
var msg string
|
|
if len(msgAndArgs) > 0 {
|
|
msg = fmt.Sprintf(msgAndArgs[0].(string)+"\n", msgAndArgs[1:]...)
|
|
}
|
|
if !assert.Equal(t, master, got, "%sMarkup did not match master. Actual markup written to file://%s.", msg, failedPath) {
|
|
require.NoError(t, writeMarkup(failedPath, got))
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Drag drags at an absolute position on the canvas.
|
|
// deltaX/Y is the dragging distance: <0 for dragging up/left, >0 for dragging down/right.
|
|
func Drag(c fyne.Canvas, pos fyne.Position, deltaX, deltaY float32) {
|
|
matches := func(object fyne.CanvasObject) bool {
|
|
if _, ok := object.(fyne.Draggable); ok {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
o, p, _ := driver.FindObjectAtPositionMatching(pos, matches, c.Overlays().Top(), c.Content())
|
|
if o == nil {
|
|
return
|
|
}
|
|
e := &fyne.DragEvent{
|
|
PointEvent: fyne.PointEvent{Position: p},
|
|
Dragged: fyne.Delta{DX: deltaX, DY: deltaY},
|
|
}
|
|
o.(fyne.Draggable).Dragged(e)
|
|
o.(fyne.Draggable).DragEnd()
|
|
}
|
|
|
|
// FocusNext focuses the next focusable on the canvas.
|
|
func FocusNext(c fyne.Canvas) {
|
|
if tc, ok := c.(*testCanvas); ok {
|
|
tc.focusManager().FocusNext()
|
|
} else {
|
|
fyne.LogError("FocusNext can only be called with a test canvas", nil)
|
|
}
|
|
}
|
|
|
|
// FocusPrevious focuses the previous focusable on the canvas.
|
|
func FocusPrevious(c fyne.Canvas) {
|
|
if tc, ok := c.(*testCanvas); ok {
|
|
tc.focusManager().FocusPrevious()
|
|
} else {
|
|
fyne.LogError("FocusPrevious can only be called with a test canvas", nil)
|
|
}
|
|
}
|
|
|
|
// LaidOutObjects returns all fyne.CanvasObject starting at the given fyne.CanvasObject which is laid out previously.
|
|
func LaidOutObjects(o fyne.CanvasObject) (objects []fyne.CanvasObject) {
|
|
if o != nil {
|
|
objects = layoutAndCollect(objects, o, o.MinSize().Max(o.Size()))
|
|
}
|
|
return objects
|
|
}
|
|
|
|
// MoveMouse simulates a mouse movement to the given position.
|
|
func MoveMouse(c fyne.Canvas, pos fyne.Position) {
|
|
if fyne.CurrentDevice().IsMobile() {
|
|
return
|
|
}
|
|
|
|
tc, _ := c.(*testCanvas)
|
|
var oldHovered, hovered desktop.Hoverable
|
|
if tc != nil {
|
|
oldHovered = tc.hovered
|
|
}
|
|
matches := func(object fyne.CanvasObject) bool {
|
|
if _, ok := object.(desktop.Hoverable); ok {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
o, p, _ := driver.FindObjectAtPositionMatching(pos, matches, c.Overlays().Top(), c.Content())
|
|
if o != nil {
|
|
hovered = o.(desktop.Hoverable)
|
|
me := &desktop.MouseEvent{
|
|
PointEvent: fyne.PointEvent{
|
|
AbsolutePosition: pos,
|
|
Position: p,
|
|
},
|
|
}
|
|
if hovered == oldHovered {
|
|
hovered.MouseMoved(me)
|
|
} else {
|
|
if oldHovered != nil {
|
|
oldHovered.MouseOut()
|
|
}
|
|
hovered.MouseIn(me)
|
|
}
|
|
} else if oldHovered != nil {
|
|
oldHovered.MouseOut()
|
|
}
|
|
if tc != nil {
|
|
tc.hovered = hovered
|
|
}
|
|
}
|
|
|
|
// Scroll scrolls at an absolute position on the canvas.
|
|
// deltaX/Y is the scrolling distance: <0 for scrolling up/left, >0 for scrolling down/right.
|
|
func Scroll(c fyne.Canvas, pos fyne.Position, deltaX, deltaY float32) {
|
|
matches := func(object fyne.CanvasObject) bool {
|
|
if _, ok := object.(fyne.Scrollable); ok {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
o, _, _ := driver.FindObjectAtPositionMatching(pos, matches, c.Overlays().Top(), c.Content())
|
|
if o == nil {
|
|
return
|
|
}
|
|
|
|
e := &fyne.ScrollEvent{Scrolled: fyne.Delta{DX: deltaX, DY: deltaY}}
|
|
o.(fyne.Scrollable).Scrolled(e)
|
|
}
|
|
|
|
// DoubleTap simulates a double left mouse click on the specified object.
|
|
func DoubleTap(obj fyne.DoubleTappable) {
|
|
ev, c := prepareTap(obj, fyne.NewPos(1, 1))
|
|
handleFocusOnTap(c, obj)
|
|
obj.DoubleTapped(ev)
|
|
}
|
|
|
|
// Tap simulates a left mouse click on the specified object.
|
|
func Tap(obj fyne.Tappable) {
|
|
TapAt(obj, fyne.NewPos(1, 1))
|
|
}
|
|
|
|
// TapAt simulates a left mouse click on the passed object at a specified place within it.
|
|
func TapAt(obj fyne.Tappable, pos fyne.Position) {
|
|
ev, c := prepareTap(obj, pos)
|
|
tap(c, obj, ev)
|
|
}
|
|
|
|
// TapCanvas taps at an absolute position on the canvas.
|
|
func TapCanvas(c fyne.Canvas, pos fyne.Position) {
|
|
if o, p := findTappable(c, pos); o != nil {
|
|
tap(c, o.(fyne.Tappable), &fyne.PointEvent{AbsolutePosition: pos, Position: p})
|
|
}
|
|
}
|
|
|
|
// TapSecondary simulates a right mouse click on the specified object.
|
|
func TapSecondary(obj fyne.SecondaryTappable) {
|
|
TapSecondaryAt(obj, fyne.NewPos(1, 1))
|
|
}
|
|
|
|
// TapSecondaryAt simulates a right mouse click on the passed object at a specified place within it.
|
|
func TapSecondaryAt(obj fyne.SecondaryTappable, pos fyne.Position) {
|
|
ev, c := prepareTap(obj, pos)
|
|
handleFocusOnTap(c, obj)
|
|
obj.TappedSecondary(ev)
|
|
}
|
|
|
|
// Type performs a series of key events to simulate typing of a value into the specified object.
|
|
// The focusable object will be focused before typing begins.
|
|
// The chars parameter will be input one rune at a time to the focused object.
|
|
func Type(obj fyne.Focusable, chars string) {
|
|
obj.FocusGained()
|
|
|
|
typeChars([]rune(chars), obj.TypedRune)
|
|
}
|
|
|
|
// TypeOnCanvas is like the Type function but it passes the key events to the canvas object
|
|
// rather than a focusable widget.
|
|
func TypeOnCanvas(c fyne.Canvas, chars string) {
|
|
typeChars([]rune(chars), c.OnTypedRune())
|
|
}
|
|
|
|
// ApplyTheme sets the given theme and waits for it to be applied to the current app.
|
|
func ApplyTheme(t *testing.T, theme fyne.Theme) {
|
|
require.IsType(t, &testApp{}, fyne.CurrentApp())
|
|
a := fyne.CurrentApp().(*testApp)
|
|
a.Settings().SetTheme(theme)
|
|
for a.lastAppliedTheme() != theme {
|
|
time.Sleep(1 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
// WidgetRenderer allows test scripts to gain access to the current renderer for a widget.
|
|
// This can be used for verifying correctness of rendered components for a widget in unit tests.
|
|
func WidgetRenderer(wid fyne.Widget) fyne.WidgetRenderer {
|
|
return cache.Renderer(wid)
|
|
}
|
|
|
|
// WithTestTheme runs a function with the testTheme temporarily set.
|
|
func WithTestTheme(t *testing.T, f func()) {
|
|
settings := fyne.CurrentApp().Settings()
|
|
current := settings.Theme()
|
|
ApplyTheme(t, NewTheme())
|
|
defer ApplyTheme(t, current)
|
|
f()
|
|
}
|
|
|
|
func findTappable(c fyne.Canvas, pos fyne.Position) (o fyne.CanvasObject, p fyne.Position) {
|
|
matches := func(object fyne.CanvasObject) bool {
|
|
_, ok := object.(fyne.Tappable)
|
|
return ok
|
|
}
|
|
o, p, _ = driver.FindObjectAtPositionMatching(pos, matches, c.Overlays().Top(), c.Content())
|
|
return
|
|
}
|
|
|
|
func prepareTap(obj interface{}, pos fyne.Position) (*fyne.PointEvent, fyne.Canvas) {
|
|
d := fyne.CurrentApp().Driver()
|
|
ev := &fyne.PointEvent{Position: pos}
|
|
var c fyne.Canvas
|
|
if co, ok := obj.(fyne.CanvasObject); ok {
|
|
c = d.CanvasForObject(co)
|
|
ev.AbsolutePosition = d.AbsolutePositionForObject(co).Add(pos)
|
|
}
|
|
return ev, c
|
|
}
|
|
|
|
func tap(c fyne.Canvas, obj fyne.Tappable, ev *fyne.PointEvent) {
|
|
handleFocusOnTap(c, obj)
|
|
obj.Tapped(ev)
|
|
}
|
|
|
|
func handleFocusOnTap(c fyne.Canvas, obj interface{}) {
|
|
if c == nil {
|
|
return
|
|
}
|
|
unfocus := true
|
|
if focus, ok := obj.(fyne.Focusable); ok {
|
|
if dis, ok := obj.(fyne.Disableable); !ok || !dis.Disabled() {
|
|
unfocus = false
|
|
if focus != c.Focused() {
|
|
unfocus = true
|
|
}
|
|
}
|
|
}
|
|
if unfocus {
|
|
c.Unfocus()
|
|
}
|
|
}
|
|
|
|
func typeChars(chars []rune, keyDown func(rune)) {
|
|
for _, char := range chars {
|
|
keyDown(char)
|
|
}
|
|
}
|
|
|
|
func writeMarkup(path string, markup string) error {
|
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(path, []byte(markup), 0644)
|
|
}
|