| Nim owns the screen, Dart owns the pixels 43a02c2 nandi yesterday | 1 | ## The screen as a pure function of the app, and the reducer that moves it. |
| 2 | import std/sequtils |
| 3 | ## |
| 4 | ## These are the tests the Clojure screens never had and could not easily |
| 5 | ## have: a screen there is hiccup over cells a host installs, so exercising |
| 6 | ## one means standing up a host. Here it is a function from a record to a |
| 7 | ## tree, and a test is a call. |
| 8 | |
| 9 | import std/[json, strutils] |
| 10 | import std/unittest |
| A message in #test, from Nim 35994d4 nandi yesterday | 11 | import frq/[ui, state, irc] |
| Nim owns the screen, Dart owns the pixels 43a02c2 nandi yesterday | 12 | import frq/screens/connect as cs |
| 13 | |
| A message in #test, from Nim 35994d4 nandi yesterday | 14 | # The reducer calls `irc.start` on a Connect, and a unit test has no business |
| 15 | # opening a socket to irc.freeq.at — it did, before this stub, and the suite |
| 16 | # failed on a machine with no network for reasons that had nothing to do with |
| 17 | # the code. The stub records what it was asked for so the tests can assert on |
| 18 | # it, which is more than the real one would have told them. |
| 19 | var dialled: seq[ConnConfig] |
| 20 | irc.connector = proc(cfg: ConnConfig) {.nimcall, gcsafe.} = |
| 21 | {.cast(gcsafe).}: dialled.add cfg |
| 22 | |
| Nim owns the screen, Dart owns the pixels 43a02c2 nandi yesterday | 23 | proc find(node: Node, tag: string): seq[Node] = |
| 24 | ## Every node with this tag, depth first. |
| 25 | if node.isNil: return |
| 26 | if node.tag == tag: result.add node |
| 27 | for c in node.children: |
| 28 | result.add c.find(tag) |
| 29 | |
| 30 | proc texts(node: Node, tag: string): seq[string] = |
| 31 | for n in node.find(tag): |
| 32 | result.add n.props{"label"}.getStr() |
| 33 | |
| 34 | suite "the connect screen": |
| 35 | setup: |
| 36 | app = initState() |
| 37 | |
| 38 | test "renders a page with the title and the server fields": |
| 39 | let t = cs.connectScreen(app) |
| 40 | check t.tag == "page" |
| 41 | check "frq" in t.texts("title") |
| 42 | check "Server" in t.texts("label") |
| 43 | check t.find("checkbutton").len == 1 |
| 44 | |
| 45 | test "is pure — twice with no dispatch is the same tree": |
| 46 | check $cs.connectScreen(app).toJson == $cs.connectScreen(app).toJson |
| 47 | |
| 48 | test "guest is the default mode and shows a nickname field": |
| 49 | let keys = cs.connectScreen(app).find("entry").mapIt(it.props{"key"}.getStr()) |
| 50 | check "nick" in keys |
| 51 | check "handle" notin keys |
| 52 | |
| 53 | test "the selected mode is the primary button, and only it": |
| 54 | let t = cs.connectScreen(app) |
| 55 | var primary: seq[string] |
| 56 | for b in t.find("button"): |
| 57 | if b.props{"kind"}.getStr() == "primary": |
| 58 | primary.add b.props{"label"}.getStr() |
| 59 | # Guest is selected; Connect is primary because it is the action. |
| 60 | check "Guest" in primary |
| 61 | check "Bluesky" notin primary |
| 62 | |
| 63 | suite "dispatch": |
| 64 | setup: |
| 65 | app = initState() |
| A message in #test, from Nim 35994d4 nandi yesterday | 66 | dialled = @[] |
| Nim owns the screen, Dart owns the pixels 43a02c2 nandi yesterday | 67 | |
| 68 | test "switching mode changes which fields are shown": |
| 69 | dispatch(%*{"id": "mode.bluesky"}) |
| 70 | let keys = cs.connectScreen(app).find("entry").mapIt(it.props{"key"}.getStr()) |
| 71 | check "handle" in keys |
| 72 | check "nick" notin keys |
| 73 | |
| 74 | test "typing into the host field lands in the tree": |
| 75 | dispatch(%*{"id": "host.change", "value": "localhost"}) |
| 76 | let host = cs.connectScreen(app).find("entry").filterIt( |
| 77 | it.props{"key"}.getStr() == "host")[0] |
| 78 | check host.props{"text"}.getStr() == "localhost" |
| 79 | |
| 80 | test "the TLS tick carries the port with it": |
| 81 | check app.formPort == "6697" |
| 82 | dispatch(%*{"id": "tls.toggle"}) |
| 83 | check not app.formTls |
| 84 | check app.formPort == "6667" |
| 85 | dispatch(%*{"id": "tls.toggle"}) |
| 86 | check app.formPort == "6697" |
| 87 | |
| 88 | test "connecting swaps the button for a spinner": |
| 89 | check cs.connectScreen(app).find("spinner").len == 0 |
| 90 | dispatch(%*{"id": "connect"}) |
| 91 | let t = cs.connectScreen(app) |
| 92 | check t.find("spinner").len == 1 |
| 93 | check "Connect" notin t.texts("button") |
| 94 | |
| 95 | test "an empty host is refused, and the error is dismissable": |
| 96 | dispatch(%*{"id": "host.change", "value": " "}) |
| 97 | dispatch(%*{"id": "connect"}) |
| 98 | check app.hasError |
| 99 | check "Dismiss" in cs.connectScreen(app).texts("button") |
| 100 | dispatch(%*{"id": "error.dismiss"}) |
| 101 | check not app.hasError |
| 102 | check "Dismiss" notin cs.connectScreen(app).texts("button") |
| 103 | |
| 104 | test "the error note keeps its place in the tree either way": |
| 105 | # The bug the stable wrapper exists for: a renderer matching children by |
| 106 | # position would patch the header into a card when the error appeared. |
| 107 | let before = cs.connectScreen(app).children.mapIt(it.tag) |
| 108 | dispatch(%*{"id": "host.change", "value": ""}) |
| 109 | dispatch(%*{"id": "connect"}) |
| 110 | check cs.connectScreen(app).children.mapIt(it.tag) == before |
| 111 | |
| 112 | test "an unknown event is ignored rather than fatal": |
| 113 | let before = $cs.connectScreen(app).toJson |
| 114 | dispatch(%*{"id": "no.such.event"}) |
| 115 | check $cs.connectScreen(app).toJson == before |
| A message in #test, from Nim 35994d4 nandi yesterday | 116 | |
| 117 | suite "connecting": |
| 118 | setup: |
| 119 | app = initState() |
| 120 | dialled = @[] |
| 121 | |
| 122 | test "Connect dials the host and port on the form": |
| 123 | dispatch(%*{"id": "host.change", "value": "irc.example.org"}) |
| 124 | dispatch(%*{"id": "connect"}) |
| 125 | check dialled.len == 1 |
| 126 | check dialled[0].host == "irc.example.org" |
| 127 | check dialled[0].port == 6697 |
| 128 | check dialled[0].tls |
| 129 | check dialled[0].nick == "frq-guest" |
| 130 | |
| 131 | test "unticking TLS dials the plain port": |
| 132 | dispatch(%*{"id": "tls.toggle"}) |
| 133 | dispatch(%*{"id": "connect"}) |
| 134 | check dialled[0].port == 6667 |
| 135 | check not dialled[0].tls |
| 136 | |
| 137 | test "a blank nickname is refused before anything is dialled": |
| 138 | dispatch(%*{"id": "nick.change", "value": " "}) |
| 139 | dispatch(%*{"id": "connect"}) |
| 140 | check dialled.len == 0 |
| 141 | check app.hasError |
| 142 | |
| 143 | test "a nonsense port falls back to the one the tick implies": |
| 144 | dispatch(%*{"id": "port.change", "value": "not-a-port"}) |
| 145 | dispatch(%*{"id": "connect"}) |
| 146 | check dialled[0].port == 6697 |
| 147 | |
| 148 | test "sending before registration does not queue a line": |
| 149 | dispatch(%*{"id": "draft.change", "value": "hello"}) |
| 150 | dispatch(%*{"id": "send"}) |
| 151 | # Still in the box: nothing was sent, and the text was not eaten. |
| 152 | check app.draft == "hello" |
| 153 | check app.messages.len == 0 |