nandi/frqpublic Fork 0
6214c7e2dcadcedf15bd028871b4df08b0a0f901
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 · 244 lines · 9.2 KBNim Blame HistoryRaw
Four screens, in Nim c69a09b nandi yesterday1## 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
Sign in with Bluesky, through the broker bdd2c3c nandi 14h ago49 test "and says what Connect will actually do":
50 # This used to assert the opposite — that the screen admitted the broker
51 # flow was not wired up — which was the honest copy while it was not.
52 # A window opening on its own is alarming without a line saying it will.
SASL, and a screen that stops lying about it e50cbc7 nandi yesterday53 s.authMode = amBluesky
Sign in with Bluesky, through the broker bdd2c3c nandi 14h ago54 let dim = cs.connectScreen(s).labels("dim-label")
55 check dim.anyIt("opens your browser" in it)
56 check dim.anyIt("never reaches this app" in it)
SASL, and a screen that stops lying about it e50cbc7 nandi yesterday57
Four screens, in Nim c69a09b nandi yesterday58 test "app-password asks for both, and says where to make one":
59 s.authMode = amAppPassword
60 let t = cs.connectScreen(s)
61 check "handle" in t.keys("entry")
62 check "app-password" in t.keys("entry")
63 check t.labels("dim-label").anyIt("App Passwords" in it)
64
65 test "a remembered session offers to be forgotten, and only then":
66 s.authMode = amBluesky
67 check "Forget saved session" notin cs.connectScreen(s).labels("button")
68 s.brokerToken = "tok"
69 check "Forget saved session" in cs.connectScreen(s).labels("button")
70
71 test "the login URL is shown while the browser is open":
72 s.authMode = amBluesky
73 s.loginUrl = "https://auth.freeq.at/x"
74 let t = cs.connectScreen(s)
75 check "https://auth.freeq.at/x" in t.labels("label")
76
77 test "the error note keeps its place whether or not there is an error":
78 # The bug the stable wrapper exists for: a renderer matching children by
79 # position would patch the header into a card when the error appeared.
80 let before = cs.connectScreen(s).children.mapIt(it.tag)
81 s.hasError = true
82 s.error = "nope"
83 check cs.connectScreen(s).children.mapIt(it.tag) == before
84 check "Dismiss" in cs.connectScreen(s).labels("button")
85
86 test "the remembered and login-url wrappers hold their place too":
87 s.authMode = amBluesky
88 let bare = cs.connectScreen(s)
89 s.brokerToken = "tok"
90 s.loginUrl = "https://x"
91 check cs.connectScreen(s).keys("vbox").filterIt(it.len > 0) ==
92 bare.keys("vbox").filterIt(it.len > 0)
93
94 test "connecting swaps Connect for a spinner and a way out":
95 s.connecting = true
96 let t = cs.connectScreen(s)
97 check t.find("spinner").len == 1
98 check "Connect" notin t.labels("button")
99 check "Cancel" in t.labels("button")
100
101suite "discover":
102 setup:
103 var s = initState()
104
105 test "lists the popular channels with their blurbs":
106 let t = ss.discoverScreen(s)
107 check t.labels("title-2") == popularChannels.mapIt(it[0])
108 check "#general" in t.keys("card")
109
110 test "offers Join for a room we are not in and Open for one we are":
111 check ss.discoverScreen(s).labels("button").countIt(it == "Join") ==
112 popularChannels.len
113 var r = initRoom("#test"); r.joined = true
114 s.rooms["#test"] = r
115 let t = ss.discoverScreen(s)
116 check "Open" in t.labels("button")
117 check t.labels("button").countIt(it == "Join") == popularChannels.len - 1
118
119 test "the tab bar is under it, with Discover selected":
120 let t = ss.discoverScreen(s)
121 s.screen = scDiscover
122 let sel = ss.discoverScreen(s).find("button")
123 .filterIt(it.props{"kind"}.getStr() == "primary")
124 .mapIt(it.props{"label"}.getStr())
125 check "Discover" in sel
126 check t.find("scroll").len == 1
127
128suite "settings":
129 setup:
130 var s = initState()
131
132 test "a guest is told so":
133 check "Guest — not signed in." in
134 ss.settingsScreen(s, connected = false, desktop = true).labels("dim-label")
135
136 test "a signed-in handle is shown, and can be forgotten":
137 s.formHandle = "alice.bsky.social"
138 s.brokerToken = "tok"
139 let t = ss.settingsScreen(s, connected = true, desktop = true)
140 check "alice.bsky.social" in t.labels("label")
141 check "Forget Bluesky session" in t.labels("button")
142
143 test "Disconnect when connected, Back to connect when not":
144 check "Disconnect" in
145 ss.settingsScreen(s, connected = true, desktop = true).labels("button")
146 check "Back to connect" in
147 ss.settingsScreen(s, connected = false, desktop = true).labels("button")
148
149 test "Quit only where there is a window to quit":
150 check "Quit" in
151 ss.settingsScreen(s, connected = false, desktop = true).labels("button")
152 check "Quit" notin
153 ss.settingsScreen(s, connected = false, desktop = false).labels("button")
154
155 test "the join/part switch reflects the cell":
156 let off = ss.settingsScreen(s, false, true).find("checkbutton")[0]
157 check not off.props{"active"}.getBool()
158 s.hideJoinPart = true
159 let on = ss.settingsScreen(s, false, true).find("checkbutton")[0]
160 check on.props{"active"}.getBool()
161
162 test "the status line is live only when connected":
163 check ss.settingsScreen(s, true, true).find("status")[0]
164 .props{"live"}.getBool()
165 check not ss.settingsScreen(s, false, true).find("status")[0]
166 .props{"live"}.getBool()
167
168import frq/screens/chats as ch
169
170suite "previewLine":
171 test "collapses whitespace so a card reads as one line":
172 check ch.previewLine("a\n b\tc") == "a b c"
173 test "truncates a pasted script rather than growing the card":
174 let long = "x".repeat(200)
175 check ch.previewLine(long).runeLen == 60
176 check ch.previewLine(long).endsWith("")
177
178 test "truncates by character, not by byte":
179 # A byte slice at 59 lands inside a multi-byte character and produces
180 # mojibake where an ellipsis was wanted. The first draft did exactly that.
181 let emoji = "😀".repeat(100)
182 let got = ch.previewLine(emoji)
183 check got.runeLen == 60
184 check got.validateUtf8 == -1
185 test "leaves a short line alone":
186 check ch.previewLine("hello there") == "hello there"
187
188suite "the chats screen":
189 setup:
190 var s = initState()
191 var r = initRoom("#test")
192 r.joined = true
193 r.messages = @[Message(frm: "alice", text: "hello")]
194 s.rooms["#test"] = r
195
196 test "an empty list says so instead of showing nothing":
197 var empty = initState()
198 check "No conversations yet — join a channel." in
199 ch.chatsScreen(empty, false).labels("dim-label")
200
201 test "a room is a card with its name and last line":
202 let t = ch.chatsScreen(s, true)
203 check "#test" in t.labels("title-2")
204 check "alice: hello" in t.labels("dim-label")
205
206 test "the title says who we are when connected":
207 check "Logged in as frq-guest" in ch.chatsScreen(s, true).labels("title")
208 check "Chats" in ch.chatsScreen(s, false).labels("title")
209
210 test "an unread count shows, with a mention marked differently":
211 var u = s.rooms["#test"]
212 u.unread = 3
213 s.rooms["#test"] = u
214 check "● 3" in ch.chatsScreen(s, true).labels("label")
215 u.mention = true
216 s.rooms["#test"] = u
217 check "◆ @ 3" in ch.chatsScreen(s, true).labels("label")
218
219 test "a channel we are not in is badged, a DM never is":
220 var away = s.rooms["#test"]
221 away.joined = false
222 s.rooms["#test"] = away
223 check "not joined" in ch.chatsScreen(s, true).labels("status")
224 var dmr = initRoom("alice")
225 s.rooms["alice"] = dmr
226 # Still only the one badge: a DM has nothing to join.
227 check ch.chatsScreen(s, true).labels("status").countIt(it == "not joined") == 1
228
229 test "the button says Message for a nick and Join for a channel":
230 s.joinInput = "#room"
231 check "Join" in ch.chatsScreen(s, true).labels("button")
232 s.joinInput = "@alice"
233 check "Message" in ch.chatsScreen(s, true).labels("button")
234
235 test "the search box offers a clear only when there is something to clear":
236 check "" notin ch.chatsScreen(s, true).labels("button")
237 s.search = "te"
238 check "" in ch.chatsScreen(s, true).labels("button")
239
240 test "search filters the list":
241 s.rooms["#other"] = initRoom("#other")
242 s.search = "oth"
243 let names = ch.chatsScreen(s, true).labels("title-2")
244 check names == @["#other"]