Remix.run Logo
adityaathalye 2 days ago

OP here. Ordinarily, I would agree with you, because PageObjects themselves are not composable, in languages belonging to the "Kingdom Of Nouns".

However, the following design, thanks to Clojure's language design, helped address a rather nasty situation.

A tightly scoped Domain Specific Language, over some Types of PageObjects, all of which compose arbitrarily (without breaking value semantics). So, if you wanted the `value` of a Modal box having all sorts of switches, form fields etc., you'd call `value` on it, and it would call `value` on all of its constituents, and return the immutable hash-map snapshot of whatever state it found.

  Cross-cutting concerns

  | v PageObject / DSL -> | open | close | open? | select | deselect | ... |
  |-----------------------+------+-------+-------+--------+----------+-----|
  | Dropdown              |      |       |       |        |          |     |
  | Checkbox              |      |       |       |        |          |     |
  | Switch                |      |       |       |        |          |     |
  | Modal                 |      |       |       |        |          |     |
  | SearchList            |      |       |       |        |          |     |
  | ...                   |      |       |       |        |          |     |
Concrete example (in the deck and demo linked below):

  (defprotocol IPageObject
    "Each PageObject MUST implement the IPageObject protocol."
    (page-object [this])
    (exists? [this])
    (visible? [this]))
And then an implementation like this:

  (defrecord Checkbox [target-css]
    IPageObject
    (page-object [this]
      this)
    (exists? [this]
      ;; webdriver check if target-css exists
      )
    (visible? [this]
      ;; webdriver check if target-css is visible
      )

    Selectable
    (select [this]
      ;; webdriver select the target
      )

    (deselect [this]
      ;; webdriver undo selection
      )
  
    (selected? [this]
      ;; webdriver return true if target selected
      )  

    Value
    (get-value [this]
      ;; webdriver return current selection state
      (selected? this)))
Deck: https://github.com/adityaathalye/slideware/blob/master/desig...

Talk + Demo: https://www.youtube.com/watch?v=hwoLON80ZzA&list=PLG4-zNACPC...

I also commented about it here: https://news.ycombinator.com/item?id=45161410 (Clojure's solution to the Expression problem).

That said, UI testing is a hot mess in general (especially for SPAs). I prefer to avoid automating anything but the "happy paths". IME, exploratory testing is better at sussing out corner cases, and "emergent" misbehaviours. So I do that, in addition to the "happy path" suites. Cue: James Bach et. al. https://www.satisfice.com/

Also I am warming up to server-generated HTML, because I can unit-test that, if I can use `hiccup` syntax + HTMX. In that case, I just make all request handlers spit out HTML fragments as Clojure data, and test those responses in my test suite.