testing.go 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. package app
  2. import (
  3. "fmt"
  4. "reflect"
  5. "github.com/maxence-charriere/go-app/v9/pkg/errors"
  6. )
  7. // TestUIDescriptor represents a descriptor that describes a UI element and its
  8. // location from its parents.
  9. type TestUIDescriptor struct {
  10. // The location of the node. It is used by the TestMatch to find the
  11. // element to test.
  12. //
  13. // If empty, the expected UI element is compared with the root of the tree.
  14. //
  15. // Otherwise, each integer represents the index of the element to traverse,
  16. // from the root's children to the element to compare
  17. Path []int
  18. // The element to compare with the element targeted by Path. Compare
  19. // behavior varies depending on the element kind.
  20. //
  21. // Simple text elements only have their text value compared.
  22. //
  23. // HTML elements have their attribute compared and check if their event
  24. // handlers are set.
  25. //
  26. // Components have their exported field values compared.
  27. Expected UI
  28. }
  29. // TestPath is a helper function that returns a path to use in a
  30. // TestUIDescriptor.
  31. func TestPath(p ...int) []int {
  32. return p
  33. }
  34. // TestMatch looks for the element targeted by the descriptor in the given tree
  35. // and reports whether it matches with the expected element.
  36. //
  37. // Eg:
  38. //
  39. // tree := app.Div().Body(
  40. // app.H2().Body(
  41. // app.Text("foo"),
  42. // ),
  43. // app.P().Body(
  44. // app.Text("bar"),
  45. // ),
  46. // )
  47. //
  48. // // Testing root:
  49. // err := app.TestMatch(tree, app.TestUIDescriptor{
  50. // Path: TestPath(),
  51. // Expected: app.Div(),
  52. // })
  53. // // OK => err == nil
  54. //
  55. // // Testing h2:
  56. // err := app.TestMatch(tree, app.TestUIDescriptor{
  57. // Path: TestPath(0),
  58. // Expected: app.H3(),
  59. // })
  60. // // KO => err != nil because we ask h2 to match with h3
  61. //
  62. // // Testing text from p:
  63. // err = app.TestMatch(tree, app.TestUIDescriptor{
  64. // Path: TestPath(1, 0),
  65. // Expected: app.Text("bar"),
  66. // })
  67. // // OK => err == nil
  68. func TestMatch(tree UI, d TestUIDescriptor) error {
  69. if d.Expected != nil {
  70. d.Expected.setSelf(d.Expected)
  71. }
  72. if len(d.Path) != 0 {
  73. idx := d.Path[0]
  74. if idx < 0 || idx >= len(tree.getChildren()) {
  75. // Check that the element does not exists.
  76. if d.Expected == nil {
  77. return nil
  78. }
  79. return errors.New("ui element to match is out of range").
  80. Tag("name", d.Expected.name()).
  81. Tag("kind", d.Expected.Kind()).
  82. Tag("parent-name", tree.name()).
  83. Tag("parent-kind", tree.Kind()).
  84. Tag("parent-children-count", len(tree.getChildren())).
  85. Tag("index", idx)
  86. }
  87. c := tree.getChildren()[idx]
  88. p := c.getParent()
  89. if p != tree {
  90. return errors.New("unexpected ui element parent").
  91. Tag("name", d.Expected.name()).
  92. Tag("kind", d.Expected.Kind()).
  93. Tag("parent-name", p.name()).
  94. Tag("parent-kind", p.Kind()).
  95. Tag("parent-addr", fmt.Sprintf("%p", p)).
  96. Tag("expected-parent-name", tree.name()).
  97. Tag("expected-parent-kind", tree.Kind()).
  98. Tag("expected-parent-addr", fmt.Sprintf("%p", tree))
  99. }
  100. d.Path = d.Path[1:]
  101. return TestMatch(c, d)
  102. }
  103. if d.Expected.name() != tree.name() || d.Expected.Kind() != tree.Kind() {
  104. return errors.New("the UI element is not matching the descriptor").
  105. Tag("expected-name", d.Expected.name()).
  106. Tag("expected-kind", d.Expected.Kind()).
  107. Tag("current-name", tree.name()).
  108. Tag("current-kind", tree.Kind())
  109. }
  110. switch d.Expected.Kind() {
  111. case SimpleText:
  112. return matchText(tree, d)
  113. case HTML:
  114. if err := matchHTMLElemAttrs(tree, d); err != nil {
  115. return err
  116. }
  117. return matchHTMLElemEventHandlers(tree, d)
  118. case Component:
  119. return matchComponent(tree, d)
  120. case RawHTML:
  121. return matchRaw(tree, d)
  122. default:
  123. return errors.New("the UI element is not matching the descriptor").
  124. Tag("reason", "unavailable matching for the kind").
  125. Tag("kind", d.Expected.Kind())
  126. }
  127. }
  128. func matchText(n UI, d TestUIDescriptor) error {
  129. a := n.(*text)
  130. b := d.Expected.(*text)
  131. if a.value != b.value {
  132. return errors.New("the text element is not matching the descriptor").
  133. Tag("name", a.name()).
  134. Tag("reason", "unexpected text value").
  135. Tag("expected-value", b.value).
  136. Tag("current-value", a.value)
  137. }
  138. return nil
  139. }
  140. func matchHTMLElemAttrs(n UI, d TestUIDescriptor) error {
  141. aAttrs := n.getAttributes()
  142. bAttrs := d.Expected.getAttributes()
  143. if len(aAttrs) != len(bAttrs) {
  144. return errors.New("the html element is not matching the descriptor").
  145. Tag("name", n.name()).
  146. Tag("reason", "unexpected attributes length").
  147. Tag("expected-attributes-length", len(bAttrs)).
  148. Tag("current-attributes-length", len(aAttrs))
  149. }
  150. for k, b := range bAttrs {
  151. a, exists := aAttrs[k]
  152. if !exists {
  153. return errors.New("the html element is not matching the descriptor").
  154. Tag("name", n.name()).
  155. Tag("reason", "an attribute is missing").
  156. Tag("attribute", k)
  157. }
  158. if a != b {
  159. return errors.New("the html element is not matching the descriptor").
  160. Tag("name", n.name()).
  161. Tag("reason", "unexpected attribute value").
  162. Tag("attribute", k).
  163. Tag("expected-value", b).
  164. Tag("current-value", a)
  165. }
  166. }
  167. for k := range bAttrs {
  168. _, exists := bAttrs[k]
  169. if !exists {
  170. return errors.New("the html element is not matching the descriptor").
  171. Tag("name", n.name()).
  172. Tag("reason", "an unexpected attribute is present").
  173. Tag("attribute", k)
  174. }
  175. }
  176. return nil
  177. }
  178. func matchHTMLElemEventHandlers(n UI, d TestUIDescriptor) error {
  179. aevents := n.getEventHandlers()
  180. bevents := d.Expected.getEventHandlers()
  181. if len(aevents) != len(bevents) {
  182. return errors.New("the html element is not matching the descriptor").
  183. Tag("name", n.name()).
  184. Tag("reason", "unexpected event handlers length").
  185. Tag("expected-event-handlers-length", len(bevents)).
  186. Tag("current-event-handlers-length", len(aevents))
  187. }
  188. for k := range bevents {
  189. _, exists := aevents[k]
  190. if !exists {
  191. return errors.New("the html element is not matching the descriptor").
  192. Tag("name", n.name()).
  193. Tag("reason", "an event handler is missing").
  194. Tag("event-handler", k)
  195. }
  196. }
  197. for k := range bevents {
  198. _, exists := aevents[k]
  199. if !exists {
  200. return errors.New("the html element is not matching the descriptor").
  201. Tag("name", n.name()).
  202. Tag("reason", "an unexpected event handler is present").
  203. Tag("event-handler", k)
  204. }
  205. }
  206. return nil
  207. }
  208. func matchComponent(n UI, d TestUIDescriptor) error {
  209. aval := reflect.ValueOf(n).Elem()
  210. bval := reflect.ValueOf(d.Expected).Elem()
  211. compotype := reflect.TypeOf(Compo{})
  212. for i := 0; i < bval.NumField(); i++ {
  213. a := aval.Field(i)
  214. b := bval.Field(i)
  215. if a.Type() == compotype {
  216. continue
  217. }
  218. if !a.CanSet() {
  219. continue
  220. }
  221. if !reflect.DeepEqual(a.Interface(), b.Interface()) {
  222. return errors.New("the component is not matching with the descriptor").
  223. Tag("name", n.name()).
  224. Tag("reason", "unexpected field value").
  225. Tag("field", bval.Type().Field(i).Name).
  226. Tag("expected-value", b.Interface()).
  227. Tag("current-value", a.Interface())
  228. }
  229. }
  230. return nil
  231. }
  232. func matchRaw(n UI, d TestUIDescriptor) error {
  233. a := n.(*raw)
  234. b := d.Expected.(*raw)
  235. if a.value != b.value {
  236. return errors.New("the raw html element is not matching with the descriptor").
  237. Tag("name", n.name()).
  238. Tag("reason", "unexpected value").
  239. Tag("expected-value", b.value).
  240. Tag("current-value", a.value)
  241. }
  242. return nil
  243. }