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