test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. package test
  2. import (
  3. "fmt"
  4. "image"
  5. "io/ioutil"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "testing"
  10. "time"
  11. "fyne.io/fyne/v2"
  12. "fyne.io/fyne/v2/driver/desktop"
  13. "fyne.io/fyne/v2/internal/cache"
  14. "fyne.io/fyne/v2/internal/driver"
  15. "fyne.io/fyne/v2/internal/painter/software"
  16. "fyne.io/fyne/v2/internal/test"
  17. "github.com/stretchr/testify/assert"
  18. "github.com/stretchr/testify/require"
  19. )
  20. // AssertCanvasTappableAt asserts that the canvas is tappable at the given position.
  21. func AssertCanvasTappableAt(t *testing.T, c fyne.Canvas, pos fyne.Position) bool {
  22. if o, _ := findTappable(c, pos); o == nil {
  23. t.Errorf("No tappable found at %#v", pos)
  24. return false
  25. }
  26. return true
  27. }
  28. // AssertObjectRendersToImage asserts that the given `CanvasObject` renders the same image as the one stored in the master file.
  29. // The theme used is the standard test theme which may look different to how it shows on your device.
  30. // The master filename is relative to the `testdata` directory which is relative to the test.
  31. // The test `t` fails if the given image is not equal to the loaded master image.
  32. // In this case the given image is written into a file in `testdata/failed/<masterFilename>` (relative to the test).
  33. // This path is also reported, thus the file can be used as new master.
  34. //
  35. // Since 2.3
  36. func AssertObjectRendersToImage(t *testing.T, masterFilename string, o fyne.CanvasObject, msgAndArgs ...interface{}) bool {
  37. c := NewCanvasWithPainter(software.NewPainter())
  38. c.SetPadded(false)
  39. size := o.MinSize().Max(o.Size())
  40. c.SetContent(o)
  41. c.Resize(size) // ensure we are large enough for current size
  42. return AssertRendersToImage(t, masterFilename, c, msgAndArgs...)
  43. }
  44. // AssertObjectRendersToMarkup asserts that the given `CanvasObject` renders the same markup as the one stored in the master file.
  45. // The master filename is relative to the `testdata` directory which is relative to the test.
  46. // The test `t` fails if the rendered markup is not equal to the loaded master markup.
  47. // In this case the rendered markup is written into a file in `testdata/failed/<masterFilename>` (relative to the test).
  48. // This path is also reported, thus the file can be used as new master.
  49. //
  50. // Be aware, that the indentation has to use tab characters ('\t') instead of spaces.
  51. // Every element starts on a new line indented one more than its parent.
  52. // Closing elements stand on their own line, too, using the same indentation as the opening element.
  53. // The only exception to this are text elements which do not contain line breaks unless the text includes them.
  54. //
  55. // Since 2.3
  56. func AssertObjectRendersToMarkup(t *testing.T, masterFilename string, o fyne.CanvasObject, msgAndArgs ...interface{}) bool {
  57. c := NewCanvas()
  58. c.SetPadded(false)
  59. size := o.MinSize().Max(o.Size())
  60. c.SetContent(o)
  61. c.Resize(size) // ensure we are large enough for current size
  62. return AssertRendersToMarkup(t, masterFilename, c, msgAndArgs...)
  63. }
  64. // AssertImageMatches asserts that the given image is the same as the one stored in the master file.
  65. // The master filename is relative to the `testdata` directory which is relative to the test.
  66. // The test `t` fails if the given image is not equal to the loaded master image.
  67. // In this case the given image is written into a file in `testdata/failed/<masterFilename>` (relative to the test).
  68. // This path is also reported, thus the file can be used as new master.
  69. func AssertImageMatches(t *testing.T, masterFilename string, img image.Image, msgAndArgs ...interface{}) bool {
  70. return test.AssertImageMatches(t, masterFilename, img, msgAndArgs...)
  71. }
  72. // AssertRendersToImage asserts that the given canvas renders the same image as the one stored in the master file.
  73. // The master filename is relative to the `testdata` directory which is relative to the test.
  74. // The test `t` fails if the given image is not equal to the loaded master image.
  75. // In this case the given image is written into a file in `testdata/failed/<masterFilename>` (relative to the test).
  76. // This path is also reported, thus the file can be used as new master.
  77. //
  78. // Since 2.3
  79. func AssertRendersToImage(t *testing.T, masterFilename string, c fyne.Canvas, msgAndArgs ...interface{}) bool {
  80. return test.AssertImageMatches(t, masterFilename, c.Capture(), msgAndArgs...)
  81. }
  82. // AssertRendersToMarkup asserts that the given canvas renders the same markup as the one stored in the master file.
  83. // The master filename is relative to the `testdata` directory which is relative to the test.
  84. // The test `t` fails if the rendered markup is not equal to the loaded master markup.
  85. // In this case the rendered markup is written into a file in `testdata/failed/<masterFilename>` (relative to the test).
  86. // This path is also reported, thus the file can be used as new master.
  87. //
  88. // Be aware, that the indentation has to use tab characters ('\t') instead of spaces.
  89. // Every element starts on a new line indented one more than its parent.
  90. // Closing elements stand on their own line, too, using the same indentation as the opening element.
  91. // The only exception to this are text elements which do not contain line breaks unless the text includes them.
  92. //
  93. // Since: 2.0
  94. func AssertRendersToMarkup(t *testing.T, masterFilename string, c fyne.Canvas, msgAndArgs ...interface{}) bool {
  95. wd, err := os.Getwd()
  96. require.NoError(t, err)
  97. got := snapshot(c)
  98. masterPath := filepath.Join(wd, "testdata", masterFilename)
  99. failedPath := filepath.Join(wd, "testdata/failed", masterFilename)
  100. _, err = os.Stat(masterPath)
  101. if os.IsNotExist(err) {
  102. require.NoError(t, writeMarkup(failedPath, got))
  103. t.Errorf("Master not found at %s. Markup written to %s might be used as master.", masterPath, failedPath)
  104. return false
  105. }
  106. raw, err := ioutil.ReadFile(masterPath)
  107. require.NoError(t, err)
  108. master := strings.ReplaceAll(string(raw), "\r", "")
  109. var msg string
  110. if len(msgAndArgs) > 0 {
  111. msg = fmt.Sprintf(msgAndArgs[0].(string)+"\n", msgAndArgs[1:]...)
  112. }
  113. if !assert.Equal(t, master, got, "%sMarkup did not match master. Actual markup written to file://%s.", msg, failedPath) {
  114. require.NoError(t, writeMarkup(failedPath, got))
  115. return false
  116. }
  117. return true
  118. }
  119. // Drag drags at an absolute position on the canvas.
  120. // deltaX/Y is the dragging distance: <0 for dragging up/left, >0 for dragging down/right.
  121. func Drag(c fyne.Canvas, pos fyne.Position, deltaX, deltaY float32) {
  122. matches := func(object fyne.CanvasObject) bool {
  123. if _, ok := object.(fyne.Draggable); ok {
  124. return true
  125. }
  126. return false
  127. }
  128. o, p, _ := driver.FindObjectAtPositionMatching(pos, matches, c.Overlays().Top(), c.Content())
  129. if o == nil {
  130. return
  131. }
  132. e := &fyne.DragEvent{
  133. PointEvent: fyne.PointEvent{Position: p},
  134. Dragged: fyne.Delta{DX: deltaX, DY: deltaY},
  135. }
  136. o.(fyne.Draggable).Dragged(e)
  137. o.(fyne.Draggable).DragEnd()
  138. }
  139. // FocusNext focuses the next focusable on the canvas.
  140. func FocusNext(c fyne.Canvas) {
  141. if tc, ok := c.(*testCanvas); ok {
  142. tc.focusManager().FocusNext()
  143. } else {
  144. fyne.LogError("FocusNext can only be called with a test canvas", nil)
  145. }
  146. }
  147. // FocusPrevious focuses the previous focusable on the canvas.
  148. func FocusPrevious(c fyne.Canvas) {
  149. if tc, ok := c.(*testCanvas); ok {
  150. tc.focusManager().FocusPrevious()
  151. } else {
  152. fyne.LogError("FocusPrevious can only be called with a test canvas", nil)
  153. }
  154. }
  155. // LaidOutObjects returns all fyne.CanvasObject starting at the given fyne.CanvasObject which is laid out previously.
  156. func LaidOutObjects(o fyne.CanvasObject) (objects []fyne.CanvasObject) {
  157. if o != nil {
  158. objects = layoutAndCollect(objects, o, o.MinSize().Max(o.Size()))
  159. }
  160. return objects
  161. }
  162. // MoveMouse simulates a mouse movement to the given position.
  163. func MoveMouse(c fyne.Canvas, pos fyne.Position) {
  164. if fyne.CurrentDevice().IsMobile() {
  165. return
  166. }
  167. tc, _ := c.(*testCanvas)
  168. var oldHovered, hovered desktop.Hoverable
  169. if tc != nil {
  170. oldHovered = tc.hovered
  171. }
  172. matches := func(object fyne.CanvasObject) bool {
  173. if _, ok := object.(desktop.Hoverable); ok {
  174. return true
  175. }
  176. return false
  177. }
  178. o, p, _ := driver.FindObjectAtPositionMatching(pos, matches, c.Overlays().Top(), c.Content())
  179. if o != nil {
  180. hovered = o.(desktop.Hoverable)
  181. me := &desktop.MouseEvent{
  182. PointEvent: fyne.PointEvent{
  183. AbsolutePosition: pos,
  184. Position: p,
  185. },
  186. }
  187. if hovered == oldHovered {
  188. hovered.MouseMoved(me)
  189. } else {
  190. if oldHovered != nil {
  191. oldHovered.MouseOut()
  192. }
  193. hovered.MouseIn(me)
  194. }
  195. } else if oldHovered != nil {
  196. oldHovered.MouseOut()
  197. }
  198. if tc != nil {
  199. tc.hovered = hovered
  200. }
  201. }
  202. // Scroll scrolls at an absolute position on the canvas.
  203. // deltaX/Y is the scrolling distance: <0 for scrolling up/left, >0 for scrolling down/right.
  204. func Scroll(c fyne.Canvas, pos fyne.Position, deltaX, deltaY float32) {
  205. matches := func(object fyne.CanvasObject) bool {
  206. if _, ok := object.(fyne.Scrollable); ok {
  207. return true
  208. }
  209. return false
  210. }
  211. o, _, _ := driver.FindObjectAtPositionMatching(pos, matches, c.Overlays().Top(), c.Content())
  212. if o == nil {
  213. return
  214. }
  215. e := &fyne.ScrollEvent{Scrolled: fyne.Delta{DX: deltaX, DY: deltaY}}
  216. o.(fyne.Scrollable).Scrolled(e)
  217. }
  218. // DoubleTap simulates a double left mouse click on the specified object.
  219. func DoubleTap(obj fyne.DoubleTappable) {
  220. ev, c := prepareTap(obj, fyne.NewPos(1, 1))
  221. handleFocusOnTap(c, obj)
  222. obj.DoubleTapped(ev)
  223. }
  224. // Tap simulates a left mouse click on the specified object.
  225. func Tap(obj fyne.Tappable) {
  226. TapAt(obj, fyne.NewPos(1, 1))
  227. }
  228. // TapAt simulates a left mouse click on the passed object at a specified place within it.
  229. func TapAt(obj fyne.Tappable, pos fyne.Position) {
  230. ev, c := prepareTap(obj, pos)
  231. tap(c, obj, ev)
  232. }
  233. // TapCanvas taps at an absolute position on the canvas.
  234. func TapCanvas(c fyne.Canvas, pos fyne.Position) {
  235. if o, p := findTappable(c, pos); o != nil {
  236. tap(c, o.(fyne.Tappable), &fyne.PointEvent{AbsolutePosition: pos, Position: p})
  237. }
  238. }
  239. // TapSecondary simulates a right mouse click on the specified object.
  240. func TapSecondary(obj fyne.SecondaryTappable) {
  241. TapSecondaryAt(obj, fyne.NewPos(1, 1))
  242. }
  243. // TapSecondaryAt simulates a right mouse click on the passed object at a specified place within it.
  244. func TapSecondaryAt(obj fyne.SecondaryTappable, pos fyne.Position) {
  245. ev, c := prepareTap(obj, pos)
  246. handleFocusOnTap(c, obj)
  247. obj.TappedSecondary(ev)
  248. }
  249. // Type performs a series of key events to simulate typing of a value into the specified object.
  250. // The focusable object will be focused before typing begins.
  251. // The chars parameter will be input one rune at a time to the focused object.
  252. func Type(obj fyne.Focusable, chars string) {
  253. obj.FocusGained()
  254. typeChars([]rune(chars), obj.TypedRune)
  255. }
  256. // TypeOnCanvas is like the Type function but it passes the key events to the canvas object
  257. // rather than a focusable widget.
  258. func TypeOnCanvas(c fyne.Canvas, chars string) {
  259. typeChars([]rune(chars), c.OnTypedRune())
  260. }
  261. // ApplyTheme sets the given theme and waits for it to be applied to the current app.
  262. func ApplyTheme(t *testing.T, theme fyne.Theme) {
  263. require.IsType(t, &testApp{}, fyne.CurrentApp())
  264. a := fyne.CurrentApp().(*testApp)
  265. a.Settings().SetTheme(theme)
  266. for a.lastAppliedTheme() != theme {
  267. time.Sleep(1 * time.Millisecond)
  268. }
  269. }
  270. // WidgetRenderer allows test scripts to gain access to the current renderer for a widget.
  271. // This can be used for verifying correctness of rendered components for a widget in unit tests.
  272. func WidgetRenderer(wid fyne.Widget) fyne.WidgetRenderer {
  273. return cache.Renderer(wid)
  274. }
  275. // WithTestTheme runs a function with the testTheme temporarily set.
  276. func WithTestTheme(t *testing.T, f func()) {
  277. settings := fyne.CurrentApp().Settings()
  278. current := settings.Theme()
  279. ApplyTheme(t, NewTheme())
  280. defer ApplyTheme(t, current)
  281. f()
  282. }
  283. func findTappable(c fyne.Canvas, pos fyne.Position) (o fyne.CanvasObject, p fyne.Position) {
  284. matches := func(object fyne.CanvasObject) bool {
  285. _, ok := object.(fyne.Tappable)
  286. return ok
  287. }
  288. o, p, _ = driver.FindObjectAtPositionMatching(pos, matches, c.Overlays().Top(), c.Content())
  289. return
  290. }
  291. func prepareTap(obj interface{}, pos fyne.Position) (*fyne.PointEvent, fyne.Canvas) {
  292. d := fyne.CurrentApp().Driver()
  293. ev := &fyne.PointEvent{Position: pos}
  294. var c fyne.Canvas
  295. if co, ok := obj.(fyne.CanvasObject); ok {
  296. c = d.CanvasForObject(co)
  297. ev.AbsolutePosition = d.AbsolutePositionForObject(co).Add(pos)
  298. }
  299. return ev, c
  300. }
  301. func tap(c fyne.Canvas, obj fyne.Tappable, ev *fyne.PointEvent) {
  302. handleFocusOnTap(c, obj)
  303. obj.Tapped(ev)
  304. }
  305. func handleFocusOnTap(c fyne.Canvas, obj interface{}) {
  306. if c == nil {
  307. return
  308. }
  309. unfocus := true
  310. if focus, ok := obj.(fyne.Focusable); ok {
  311. if dis, ok := obj.(fyne.Disableable); !ok || !dis.Disabled() {
  312. unfocus = false
  313. if focus != c.Focused() {
  314. unfocus = true
  315. }
  316. }
  317. }
  318. if unfocus {
  319. c.Unfocus()
  320. }
  321. }
  322. func typeChars(chars []rune, keyDown func(rune)) {
  323. for _, char := range chars {
  324. keyDown(char)
  325. }
  326. }
  327. func writeMarkup(path string, markup string) error {
  328. if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
  329. return err
  330. }
  331. return ioutil.WriteFile(path, []byte(markup), 0644)
  332. }