nandi/frqpublic Fork 0
60bbd359ff92543e6cd76ee5fffd0d89a07c4ae9
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.

nim_layout_test.dart · 273 lines · 10.4 KBDart Blame HistoryRaw
Lay every screen out in a test, and fix what that found 36bdfc5 nandi yesterday1/// Every screen, laid out for real, at sizes that squeeze.
2///
3/// This is the test that was missing. `Cannot hit test a render box that has
4/// never been laid out` is what a failed layout looks like from the outside,
5/// and nothing automated ever laid the chat screen out — a GUI on Wayland
6/// cannot be clicked, so every check stopped at the room list while the
7/// biggest screen in the app went out unverified.
8///
9/// `tester.takeException()` is the whole point: a layout error is reported to
10/// FlutterError rather than thrown at the caller, so a test that only pumps
11/// and asserts on widgets passes while the screen is broken. These fail.
12///
13/// just test layout
Quality pass: reuse, dead weight, and two real costs 4dfc719 nandi yesterday14library;
15
Both ends of a scroll, on the same controller 5594a62 nandi 15h ago16import 'package:flutter/gestures.dart';
Lay every screen out in a test, and fix what that found 36bdfc5 nandi yesterday17import 'package:flutter/material.dart';
18import 'package:flutter_test/flutter_test.dart';
19import 'package:frq_core/frq_core.dart' as core;
The last of the Clojure 284b59c nandi 18h ago20import 'package:frq/nim_renderer.dart';
Lay every screen out in a test, and fix what that found 36bdfc5 nandi yesterday21
22/// Phone, small desktop, and a deliberately cramped one. The head row of the
23/// chat screen asks for more than 360 points has, which is why it wraps.
24const sizes = <String, Size>{
25 'phone': Size(360, 690),
26 'desktop': Size(1280, 800),
27 'cramped': Size(300, 500),
28};
29
30Future<void> layOut(WidgetTester tester, Size size) async {
31 await tester.binding.setSurfaceSize(size);
32 addTearDown(() => tester.binding.setSurfaceSize(null));
33 await tester.pumpWidget(const NimApp());
34 await tester.pump();
35}
36
37/// Nothing went to FlutterError while that frame was built.
38void expectLaidOut(WidgetTester tester, String what) {
39 final e = tester.takeException();
40 expect(e, isNull, reason: '$what reported: $e');
41}
42
43void main() {
44 // No offline guard needed: `demoUi` sets the state directly and none of
45 // these dispatch `connect`, so nothing here opens a socket.
46
47 group('the connect screen', () {
48 for (final entry in sizes.entries) {
49 testWidgets('lays out at ${entry.key}', (tester) async {
50 core.resetUi();
51 await layOut(tester, entry.value);
52 expectLaidOut(tester, 'connect at ${entry.key}');
53 });
54 }
55
56 testWidgets('lays out in every auth mode', (tester) async {
57 for (final mode in ['guest', 'bluesky', 'app-password']) {
58 core.resetUi();
59 core.dispatch('mode.$mode');
60 await layOut(tester, sizes['phone']!);
61 expectLaidOut(tester, 'connect in $mode');
62 }
63 });
64 });
65
66 group('the chat screen', () {
67 // The one that was never laid out by anything automated.
68 for (final entry in sizes.entries) {
69 testWidgets('lays out at ${entry.key}', (tester) async {
70 core.demoUi();
71 await layOut(tester, entry.value);
72 expectLaidOut(tester, 'chat at ${entry.key}');
73 });
74 }
75
76 testWidgets('renders the conversation it was given', (tester) async {
77 core.demoUi();
78 await layOut(tester, sizes['desktop']!);
79 expect(find.text('hello there'), findsOneWidget);
80 expect(find.text('#test'), findsWidgets);
81 expectLaidOut(tester, 'chat content');
82 });
83
84 testWidgets('lays out with the people panel up', (tester) async {
85 core.demoUi();
86 core.dispatch('users.toggle');
87 await layOut(tester, sizes['desktop']!);
88 expectLaidOut(tester, 'chat with people');
89 });
90
91 testWidgets('lays out with every compose banner showing', (tester) async {
92 core.demoUi();
93 core.dispatch('reply.to:2');
94 await layOut(tester, sizes['phone']!);
95 expectLaidOut(tester, 'chat replying');
96
97 core.demoUi();
98 core.dispatch('edit.start:7');
99 await layOut(tester, sizes['phone']!);
100 expectLaidOut(tester, 'chat editing');
101 });
102
Three buttons that did nothing now do what they say 98d6135 nandi 17h ago103 testWidgets('lays out with the emoji picker open', (tester) async {
104 // 120 emoji in a grid under a message, on a phone.
105 core.demoUi();
106 core.dispatch('react.open:2');
107 await layOut(tester, sizes['phone']!);
108 expectLaidOut(tester, 'chat with the picker');
109 });
110
111 testWidgets('lays out with the picker showing a whole group',
112 (tester) async {
113 core.demoUi();
114 core.dispatch('react.open:2');
115 core.dispatch('emoji.group:Smileys & Emotion');
116 await layOut(tester, sizes['cramped']!);
117 expectLaidOut(tester, 'chat with a full picker');
118 });
119
120 testWidgets('lays out with the overview open', (tester) async {
121 core.demoUi();
122 core.dispatch('overview.toggle');
123 await layOut(tester, sizes['phone']!);
124 expectLaidOut(tester, 'chat with the overview');
125 });
126
127 testWidgets('lays out with the lightbox open', (tester) async {
128 core.demoUi();
129 core.dispatch('lightbox:https://example.com/a.png');
130 await layOut(tester, sizes['phone']!);
131 expectLaidOut(tester, 'chat with the lightbox');
132 });
133
A face that opens someone 9bb81a1 nandi 17h ago134 testWidgets('lays out with a profile open', (tester) async {
135 core.demoUi();
136 // A guest: no identity to fetch, so the panel says so rather than
137 // spinning — and it lays out without a network.
138 core.dispatch('profile.open:alice:');
139 await layOut(tester, sizes['phone']!);
140 expectLaidOut(tester, 'chat with a guest profile');
141 });
142
Lay every screen out in a test, and fix what that found 36bdfc5 nandi yesterday143 testWidgets('lays out when scrolled off the present', (tester) async {
144 core.demoUi();
145 core.dispatch('jump.present');
146 await layOut(tester, sizes['phone']!);
147 expectLaidOut(tester, 'chat jumping');
148 });
149 });
150
151 group('the chats list', () {
152 for (final entry in sizes.entries) {
153 testWidgets('lays out at ${entry.key}', (tester) async {
154 core.demoUi();
155 core.dispatch('screen.chats');
156 await layOut(tester, entry.value);
157 expectLaidOut(tester, 'chats at ${entry.key}');
158 });
159 }
160
161 testWidgets('lays out with a search term in the box', (tester) async {
162 core.demoUi();
163 core.dispatch('screen.chats');
164 core.dispatch('search.change', 'te');
165 await layOut(tester, sizes['phone']!);
166 expectLaidOut(tester, 'chats searching');
167 });
168 });
169
170 group('discover and settings', () {
171 for (final screen in ['discover', 'settings']) {
172 for (final entry in sizes.entries) {
173 testWidgets('$screen lays out at ${entry.key}', (tester) async {
174 core.demoUi();
175 core.dispatch('screen.$screen');
176 await layOut(tester, entry.value);
177 expectLaidOut(tester, '$screen at ${entry.key}');
178 });
179 }
180 }
181 });
The pencil was drawn by a text font 1cb40af nandi 16h ago182
Both ends of a scroll, on the same controller 5594a62 nandi 15h ago183 group('the wheel', () {
184 // Laying a screen out was never enough to catch this one: the failure
185 // arrives on the first wheel event, not on the first frame. A `Scrollbar`
186 // with no controller asks the PrimaryScrollController, and a
187 // SingleChildScrollView is only primary on mobile — so on a desktop the
188 // scrollbar and the view held different controllers, and every scroll
189 // threw "has no ScrollPosition attached".
190 Future<void> wheelOver(WidgetTester tester, Finder target) async {
191 final pointer = TestPointer(1, PointerDeviceKind.mouse);
192 pointer.hover(tester.getCenter(target));
193 await tester.sendEventToBinding(pointer.scroll(const Offset(0, 60)));
194 await tester.pump();
195 }
196
197 for (final screen in ['chat', 'chats', 'discover', 'settings']) {
198 // The platform has to be said out loud. A widget test runs as Android
199 // by default, where a SingleChildScrollView *is* primary and attaches
200 // to the very controller the scrollbar is looking at — so this passed
201 // on the broken renderer while the desktop app threw on every wheel
202 // event. `variant` rather than an override this test resets itself,
203 // which the framework catches as a leaked debug variable.
204 testWidgets('$screen scrolls without losing its scrollbar',
205 variant: TargetPlatformVariant.desktop(), (tester) async {
206 core.demoUi();
207 if (screen != 'chat') core.dispatch('screen.$screen');
208 await layOut(tester, sizes['desktop']!);
209 expectLaidOut(tester, '$screen before scrolling');
210 final bars = find.byType(Scrollbar);
211 expect(bars, findsWidgets, reason: '$screen has nothing to scroll');
212 await wheelOver(tester, bars.first);
213 expectLaidOut(tester, '$screen on the wheel');
214 });
215 }
216 });
217
Text can be selected ec74784 nandi 14h ago218 group('selection', () {
219 testWidgets('the whole tree sits in one SelectionArea', (tester) async {
220 // One, not one per Text: a selection has to be draggable across the
221 // nick, the time and the message, which is most of what anyone wants
222 // to copy out of a chat.
223 core.demoUi();
224 await layOut(tester, sizes['desktop']!);
225 expect(find.byType(SelectionArea), findsOneWidget);
226 expect(find.text('hello there'), findsOneWidget);
227 });
228
229 testWidgets('and taps still reach what is under it', (tester) async {
230 // The risk with wrapping everything: a selection gesture that eats the
231 // taps underneath. Opening a room from the list is the plainest one.
232 core.demoUi();
233 core.dispatch('screen.chats');
234 await layOut(tester, sizes['desktop']!);
235 // The row's "Open", not its name: the name is a label in this list.
236 await tester.tap(find.text('Open').first);
237 await tester.pump();
238 expect(find.text('hello there'), findsWidgets,
239 reason: 'tapping the room did not open it');
240 expectLaidOut(tester, 'the room the tap opened');
241 });
242 });
243
The pencil was drawn by a text font 1cb40af nandi 16h ago244 group('emoji', () {
245 // ✏️ is U+270F plus a variation selector asking for emoji presentation,
246 // and DejaVu Sans claims U+270F — so ordinary fallback draws a monochrome
247 // pencil and never reaches the emoji font. Naming the font is the fix,
248 // and this is the assertion that it is still named.
249 //
250 // Emoji *presentation*, which is narrower than "not a letter". A glyph
251 // carrying U+FE0F is asking for it, and so is anything from the emoji
252 // blocks. The arrows and crosses on buttons — → ✕ ☰ — are not: they are
253 // text glyphs on purpose and take the text font, as does the reply chip's
254 // "↩ me: a picture", where the arrow sits in a sentence.
255
256 testWidgets('a lone glyph is drawn in the colour emoji font',
257 (tester) async {
258 core.demoUi();
259 await layOut(tester, sizes['desktop']!);
260 final glyphs = tester
261 .widgetList<Text>(find.byType(Text))
262 .where((t) =>
263 t.data != null &&
264 RegExp(r'[\ufe0f\u{1f300}-\u{1faff}]', unicode: true)
265 .hasMatch(t.data!));
266 expect(glyphs, isNotEmpty, reason: 'the chat screen draws no emoji');
267 for (final g in glyphs) {
268 expect(g.style?.fontFamilyFallback, contains('Noto Color Emoji'),
269 reason: 'a bare glyph without the emoji font: ${g.data}');
270 }
271 });
272 });
Lay every screen out in a test, and fix what that found 36bdfc5 nandi yesterday273}