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