nandi/frqpublic Fork 0
284b59c6810a1b2abace25c071e9d166cd0dafc9
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

tscreens.nim · 240 lines · 8.9 KBNim Blame HistoryRaw
Four screens, in Nim c69a09b nandi 18h ago1## The screens, as pure functions of the state.
2##
3## These are the tests the ClojureDart screens never had and could not easily
4## have: a screen there is hiccup over cells a host installs, so exercising one
5## means standing up a host. Here it is a function from a record to a tree.
6
7import std/[json, sequtils, strutils, tables, unicode, unittest]
8import frq/[ui, cells, model]
9import frq/screens/connect as cs
10import frq/screens/settings as ss
11
12proc find*(node: Node, tag: string): seq[Node] =
13 if node.isNil: return
14 if node.tag == tag: result.add node
15 for c in node.children: result.add c.find(tag)
16
17proc labels(node: Node, tag: string): seq[string] =
18 node.find(tag).mapIt(it.props{"label"}.getStr())
19
20proc keys(node: Node, tag: string): seq[string] =
21 node.find(tag).mapIt(it.props{"key"}.getStr())
22
23suite "the connect screen":
24 setup:
25 var s = initState()
26
27 test "is a page with the title and the transport note":
28 let t = cs.connectScreen(s)
29 check t.tag == "page"
30 check "frq" in t.labels("title")
31 check cs.transportNote in t.labels("dim-label")
32
33 test "is pure — twice with no change is the same tree":
34 check $cs.connectScreen(s).toJson == $cs.connectScreen(s).toJson
35
36 test "guest is the default, and shows a nick field":
37 let t = cs.connectScreen(s)
38 check "Connect as guest" in t.labels("title-2")
39 check "nick" in t.keys("entry")
40 check "handle" notin t.keys("entry")
41
42 test "bluesky shows a handle and its own copy":
43 s.authMode = amBluesky
44 let t = cs.connectScreen(s)
45 check "Sign in with Bluesky" in t.labels("title-2")
46 check "handle" in t.keys("entry")
47 check "nick" notin t.keys("entry")
48
SASL, and a screen that stops lying about it e50cbc7 nandi 17h ago49 test "and admits the broker flow is not wired up":
50 # A screen that describes the finished thing is a screen that lies.
51 s.authMode = amBluesky
52 check cs.connectScreen(s).labels("dim-label").anyIt("Not wired up yet" in it)
53
Four screens, in Nim c69a09b nandi 18h ago54 test "app-password asks for both, and says where to make one":
55 s.authMode = amAppPassword
56 let t = cs.connectScreen(s)
57 check "handle" in t.keys("entry")
58 check "app-password" in t.keys("entry")
59 check t.labels("dim-label").anyIt("App Passwords" in it)
60
61 test "a remembered session offers to be forgotten, and only then":
62 s.authMode = amBluesky
63 check "Forget saved session" notin cs.connectScreen(s).labels("button")
64 s.brokerToken = "tok"
65 check "Forget saved session" in cs.connectScreen(s).labels("button")
66
67 test "the login URL is shown while the browser is open":
68 s.authMode = amBluesky
69 s.loginUrl = "https://auth.freeq.at/x"
70 let t = cs.connectScreen(s)
71 check "https://auth.freeq.at/x" in t.labels("label")
72
73 test "the error note keeps its place whether or not there is an error":
74 # The bug the stable wrapper exists for: a renderer matching children by
75 # position would patch the header into a card when the error appeared.
76 let before = cs.connectScreen(s).children.mapIt(it.tag)
77 s.hasError = true
78 s.error = "nope"
79 check cs.connectScreen(s).children.mapIt(it.tag) == before
80 check "Dismiss" in cs.connectScreen(s).labels("button")
81
82 test "the remembered and login-url wrappers hold their place too":
83 s.authMode = amBluesky
84 let bare = cs.connectScreen(s)
85 s.brokerToken = "tok"
86 s.loginUrl = "https://x"
87 check cs.connectScreen(s).keys("vbox").filterIt(it.len > 0) ==
88 bare.keys("vbox").filterIt(it.len > 0)
89
90 test "connecting swaps Connect for a spinner and a way out":
91 s.connecting = true
92 let t = cs.connectScreen(s)
93 check t.find("spinner").len == 1
94 check "Connect" notin t.labels("button")
95 check "Cancel" in t.labels("button")
96
97suite "discover":
98 setup:
99 var s = initState()
100
101 test "lists the popular channels with their blurbs":
102 let t = ss.discoverScreen(s)
103 check t.labels("title-2") == popularChannels.mapIt(it[0])
104 check "#general" in t.keys("card")
105
106 test "offers Join for a room we are not in and Open for one we are":
107 check ss.discoverScreen(s).labels("button").countIt(it == "Join") ==
108 popularChannels.len
109 var r = initRoom("#test"); r.joined = true
110 s.rooms["#test"] = r
111 let t = ss.discoverScreen(s)
112 check "Open" in t.labels("button")
113 check t.labels("button").countIt(it == "Join") == popularChannels.len - 1
114
115 test "the tab bar is under it, with Discover selected":
116 let t = ss.discoverScreen(s)
117 s.screen = scDiscover
118 let sel = ss.discoverScreen(s).find("button")
119 .filterIt(it.props{"kind"}.getStr() == "primary")
120 .mapIt(it.props{"label"}.getStr())
121 check "Discover" in sel
122 check t.find("scroll").len == 1
123
124suite "settings":
125 setup:
126 var s = initState()
127
128 test "a guest is told so":
129 check "Guest — not signed in." in
130 ss.settingsScreen(s, connected = false, desktop = true).labels("dim-label")
131
132 test "a signed-in handle is shown, and can be forgotten":
133 s.formHandle = "alice.bsky.social"
134 s.brokerToken = "tok"
135 let t = ss.settingsScreen(s, connected = true, desktop = true)
136 check "alice.bsky.social" in t.labels("label")
137 check "Forget Bluesky session" in t.labels("button")
138
139 test "Disconnect when connected, Back to connect when not":
140 check "Disconnect" in
141 ss.settingsScreen(s, connected = true, desktop = true).labels("button")
142 check "Back to connect" in
143 ss.settingsScreen(s, connected = false, desktop = true).labels("button")
144
145 test "Quit only where there is a window to quit":
146 check "Quit" in
147 ss.settingsScreen(s, connected = false, desktop = true).labels("button")
148 check "Quit" notin
149 ss.settingsScreen(s, connected = false, desktop = false).labels("button")
150
151 test "the join/part switch reflects the cell":
152 let off = ss.settingsScreen(s, false, true).find("checkbutton")[0]
153 check not off.props{"active"}.getBool()
154 s.hideJoinPart = true
155 let on = ss.settingsScreen(s, false, true).find("checkbutton")[0]
156 check on.props{"active"}.getBool()
157
158 test "the status line is live only when connected":
159 check ss.settingsScreen(s, true, true).find("status")[0]
160 .props{"live"}.getBool()
161 check not ss.settingsScreen(s, false, true).find("status")[0]
162 .props{"live"}.getBool()
163
164import frq/screens/chats as ch
165
166suite "previewLine":
167 test "collapses whitespace so a card reads as one line":
168 check ch.previewLine("a\n b\tc") == "a b c"
169 test "truncates a pasted script rather than growing the card":
170 let long = "x".repeat(200)
171 check ch.previewLine(long).runeLen == 60
172 check ch.previewLine(long).endsWith("")
173
174 test "truncates by character, not by byte":
175 # A byte slice at 59 lands inside a multi-byte character and produces
176 # mojibake where an ellipsis was wanted. The first draft did exactly that.
177 let emoji = "😀".repeat(100)
178 let got = ch.previewLine(emoji)
179 check got.runeLen == 60
180 check got.validateUtf8 == -1
181 test "leaves a short line alone":
182 check ch.previewLine("hello there") == "hello there"
183
184suite "the chats screen":
185 setup:
186 var s = initState()
187 var r = initRoom("#test")
188 r.joined = true
189 r.messages = @[Message(frm: "alice", text: "hello")]
190 s.rooms["#test"] = r
191
192 test "an empty list says so instead of showing nothing":
193 var empty = initState()
194 check "No conversations yet — join a channel." in
195 ch.chatsScreen(empty, false).labels("dim-label")
196
197 test "a room is a card with its name and last line":
198 let t = ch.chatsScreen(s, true)
199 check "#test" in t.labels("title-2")
200 check "alice: hello" in t.labels("dim-label")
201
202 test "the title says who we are when connected":
203 check "Logged in as frq-guest" in ch.chatsScreen(s, true).labels("title")
204 check "Chats" in ch.chatsScreen(s, false).labels("title")
205
206 test "an unread count shows, with a mention marked differently":
207 var u = s.rooms["#test"]
208 u.unread = 3
209 s.rooms["#test"] = u
210 check "● 3" in ch.chatsScreen(s, true).labels("label")
211 u.mention = true
212 s.rooms["#test"] = u
213 check "◆ @ 3" in ch.chatsScreen(s, true).labels("label")
214
215 test "a channel we are not in is badged, a DM never is":
216 var away = s.rooms["#test"]
217 away.joined = false
218 s.rooms["#test"] = away
219 check "not joined" in ch.chatsScreen(s, true).labels("status")
220 var dmr = initRoom("alice")
221 s.rooms["alice"] = dmr
222 # Still only the one badge: a DM has nothing to join.
223 check ch.chatsScreen(s, true).labels("status").countIt(it == "not joined") == 1
224
225 test "the button says Message for a nick and Join for a channel":
226 s.joinInput = "#room"
227 check "Join" in ch.chatsScreen(s, true).labels("button")
228 s.joinInput = "@alice"
229 check "Message" in ch.chatsScreen(s, true).labels("button")
230
231 test "the search box offers a clear only when there is something to clear":
232 check "" notin ch.chatsScreen(s, true).labels("button")
233 s.search = "te"
234 check "" in ch.chatsScreen(s, true).labels("button")
235
236 test "search filters the list":
237 s.rooms["#other"] = initRoom("#other")
238 s.search = "oth"
239 let names = ch.chatsScreen(s, true).labels("title-2")
240 check names == @["#other"]