| The renderer, and a transport bug that took four tries 58e6c97 nandi 18h ago | 1 | /// The whole stack, end to end: Nim's socket, Nim's state, Nim's screens. |
| 2 | /// |
| 3 | /// Connects to a real freeq, waits for the room list to fill, opens a room and |
| 4 | /// prints what the tree actually contains. Everything goes through the FFI, so |
| 5 | /// what it proves is the same path the window uses. |
| 6 | /// |
| 7 | /// Not in any test suite: it needs a network and a running freeq. |
| 8 | /// |
| 9 | /// just nim-live |
| 10 | import 'dart:io'; |
| 11 | import 'package:frq_core/frq_core.dart' as core; |
| 12 | |
| 13 | List<core.UiNode> find(core.UiNode n, String tag) => |
| 14 | [if (n.tag == tag) n, for (final c in n.children) ...find(c, tag)]; |
| 15 | |
| 16 | List<String> labels(core.UiNode n, String tag) => |
| 17 | find(n, tag).map((e) => e.prop('label', '')).toList(); |
| 18 | |
| 19 | Future<void> main(List<String> args) async { |
| 20 | final host = args.isNotEmpty ? args[0] : 'irc.freeq.at'; |
| 21 | final nick = args.length > 1 |
| 22 | ? args[1] |
| 23 | : 'frq-ui-${DateTime.now().millisecondsSinceEpoch % 10000}'; |
| 24 | |
| 25 | core.resetUi(); |
| 26 | print('→ $host as $nick'); |
| 27 | |
| 28 | core.dispatch('nick.change', nick); |
| 29 | core.dispatch('host.change', host); |
| 30 | var tree = core.dispatch('connect'); |
| 31 | |
| 32 | final deadline = DateTime.now().add(const Duration(seconds: 25)); |
| 33 | while (DateTime.now().isBefore(deadline)) { |
| 34 | await Future<void>.delayed(const Duration(milliseconds: 100)); |
| 35 | tree = core.poll(); |
| 36 | if (labels(tree, 'title').any((l) => l.startsWith('Logged in as'))) break; |
| 37 | final err = labels(tree, 'label').where((l) => l.startsWith('⚠')); |
| 38 | if (err.isNotEmpty) { |
| 39 | print('✗ ${err.first}'); |
| 40 | exit(1); |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | if (!labels(tree, 'title').any((l) => l.startsWith('Logged in as'))) { |
| 45 | print('✗ never reached the chats screen — still ${labels(tree, "title")}'); |
| 46 | exit(1); |
| 47 | } |
| 48 | print('✓ chats screen: ${labels(tree, "title").first}'); |
| 49 | |
| 50 | // The room list, from the real server. Waited for rather than read on the |
| 51 | // instant we land: registration finishes before the JOIN echo that creates |
| 52 | // the buffer, so reading immediately is reading too early. |
| 53 | final roomsBy = DateTime.now().add(const Duration(seconds: 10)); |
| 54 | var rooms = <String>[]; |
| 55 | while (DateTime.now().isBefore(roomsBy)) { |
| 56 | await Future<void>.delayed(const Duration(milliseconds: 200)); |
| 57 | tree = core.poll(); |
| 58 | rooms = labels(tree, 'title-2'); |
| 59 | if (rooms.isNotEmpty) break; |
| 60 | } |
| 61 | print('✓ rooms: $rooms'); |
| 62 | if (rooms.isEmpty) { |
| 63 | print('✗ no rooms in the list'); |
| 64 | exit(1); |
| 65 | } |
| 66 | |
| 67 | // Open one and look at the conversation. |
| 68 | tree = core.dispatch('room.open:${rooms.first}'); |
| 69 | await Future<void>.delayed(const Duration(seconds: 2)); |
| 70 | tree = core.poll(); |
| 71 | |
| 72 | print('✓ chat screen: ${labels(tree, "title").first}'); |
| 73 | final said = find(tree, 'text').map((e) => e.prop('text', '')).toList(); |
| 74 | print('✓ ${said.length} text runs, ${find(tree, "link").length} links, ' |
| 75 | '${find(tree, "avatar").length} avatars, ' |
| 76 | '${find(tree, "reaction").length} reaction chips, ' |
| 77 | '${find(tree, "separator").length} separators'); |
| 78 | for (final line in said.take(6)) { |
| 79 | print(' | $line'); |
| 80 | } |
| 81 | |
| 82 | // Every tag the tree contains, so an unrendered one shows up here rather |
| 83 | // than as an orange box in the window. |
| 84 | final tags = <String>{}; |
| 85 | void walk(core.UiNode n) { |
| 86 | tags.add(n.tag); |
| 87 | for (final c in n.children) { |
| 88 | walk(c); |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | walk(tree); |
| 93 | print('✓ tags in use: ${(tags.toList()..sort()).join(", ")}'); |
| 94 | |
| 95 | core.dispatch('disconnect'); |
| 96 | print('✓ disconnected'); |
| 97 | } |