| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371 |
- 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)
- }
|