Files
pyramid/test/live_ui_fixture_test.dart
Bernd SteckmeisterandClaude Opus 5.5 c5f10901e6 feat: UI-Umbau - weiches Dunkel, Schriftgroessen-Regler, Sprechblasen umschaltbar, Einstellungen als Vollbild
Bernds Entscheidungen 2026-10-07:
- theme.dart: ThemeVariant (Dunkel = neues weiches Dunkelgrau als Standard,
  Schwarz = bisheriges Fast-Schwarz, Hell) mit PyramidPalette; Nebentexte
  kontrastreicher. Alte Einstellung theme_dark wird uebernommen
  (dunkel -> weiches Dunkel). Fest verdrahtete Dunkelfarben im Profil-Popup
  und GIF-Picker folgen jetzt dem Farbmodus.
- appearance.dart: Schriftgroesse = Pyramid-Regler x Systemeinstellung
  (Handy-Standard 115 %, PC 100 %), MessageStyle Automatisch/Sprechblasen/
  Liste, gemeinsame isMobileLayout-Regel.
- message_bubbles.dart: Sprechblasen-Darstellung (eigene rechts in
  Akzentfarbe, Gruppen mit Name + Avatar, Bilder ohne Blase in voller Groesse);
  Listen-Darstellung unveraendert, Weiche in MessageGroup.
- Formatierte Nachrichten (u. a. Antworten) nutzten RichText ohne
  Schriftskalierung -> erschienen kleiner; jetzt Text.rich.
- Einstellungen auf dem Handy als ganze Seite, Schalter unter dem Text.
- UI-Durchlauf um Liste/Hell/Gruppe/Einstellungen erweitert; Tests fuer
  Theme-Varianten, Uebernahme alter Werte, Schriftfaktor, Darstellung.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 17:16:07 +02:00

255 lines
8.3 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@TestOn('windows')
library;
import 'dart:ffi';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:image/image.dart' as img;
import 'package:matrix/matrix.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:sqlite3/open.dart' as sqlite_open;
/// Setup-Helfer für UI-Prüfungen (gegated wie die anderen Live-Tests, läuft
/// NIE im normalen `flutter test`): pyramidtest2 schickt pyramidtest1 eine
/// Direktnachricht mit typischem Chat-Inhalt – langer Text,
/// Antwort, Hochkant- und Querformat-Bild. Danach zeigt die GUI-Instanz mit
/// dem Testprofil (PYRAMID_PROFILE_DIR=…\profile1) echte Inhalte, an denen
/// sich Layout, Bildgrößen und Vollansicht prüfen lassen.
///
/// $env:PYRAMID_LIVE_TEST=1; flutter test test/live_ui_fixture_test.dart
///
/// Der Raum ist unverschlüsselt (der Host-Test hat kein vodozemac). Nur die
/// Skript-Geräte melden sich am Ende ab, GUI-Sessions bleiben unberührt.
Map<String, String>? _loadAccounts() {
final override = Platform.environment['PYRAMID_TEST_ACCOUNTS'];
final profile = Platform.environment['USERPROFILE'] ?? '';
final file = File(
override ?? '$profile\\.pyramid-autopilot\\test-accounts.txt',
);
if (!file.existsSync()) return null;
final map = <String, String>{};
for (final line in file.readAsLinesSync()) {
final t = line.trim();
if (t.isEmpty || t.startsWith('#')) continue;
final i = t.indexOf('=');
if (i > 0) map[t.substring(0, i)] = t.substring(i + 1);
}
return map;
}
String? _findSqliteDll() {
for (final config in ['Debug', 'Profile', 'Release']) {
final file = File('build/windows/x64/runner/$config/sqlite3.dll');
if (file.existsSync()) return file.absolute.path;
}
return null;
}
Future<Client> _login(String name, String user, String pass, Uri hs) async {
final dll = _findSqliteDll()!;
final factory = createDatabaseFactoryFfi(
ffiInit: () {
sqlite_open.open.overrideFor(
sqlite_open.OperatingSystem.windows,
() => DynamicLibrary.open(dll),
);
},
noIsolate: true,
);
final db = await factory.openDatabase(inMemoryDatabasePath);
final client = Client(
name,
database: await MatrixSdkDatabase.init(name, database: db),
);
await client.init(
waitForFirstSync: false,
waitUntilLoadCompletedLoaded: false,
);
await client.checkHomeserver(hs);
await client.login(
LoginType.mLoginPassword,
identifier: AuthenticationUserIdentifier(user: user),
password: pass,
initialDeviceDisplayName: name,
);
return client;
}
/// Logout ohne verspätete 401 des laufenden Long-Polls (siehe
/// live_encryption_e2e_test.dart, `_logoutWithoutStraySync`).
Future<void> _logout(Client c) async {
try {
c.backgroundSync = false;
await c
.oneShotSync(timeout: Duration.zero)
.timeout(const Duration(seconds: 60));
await c.logout();
} catch (_) {}
}
/// Einfaches, gut erkennbares Testbild (Farbfläche + Kreis + Rahmen).
Uint8List _testImage(int w, int h, img.Color bg) {
final image = img.Image(width: w, height: h);
img.fill(image, color: bg);
img.fillCircle(
image,
x: w ~/ 2,
y: h ~/ 2,
radius: (w < h ? w : h) ~/ 4,
color: img.ColorRgb8(245, 166, 20),
);
img.drawRect(
image,
x1: 0,
y1: 0,
x2: w - 1,
y2: h - 1,
color: img.ColorRgb8(255, 255, 255),
thickness: 6,
);
return Uint8List.fromList(img.encodeJpg(image, quality: 85));
}
void main() {
final acc = _loadAccounts();
final skip = Platform.environment['PYRAMID_LIVE_TEST'] != '1'
? 'Live-Setup nur mit PYRAMID_LIVE_TEST=1'
: acc == null
? 'Test-Account-Datei fehlt'
: _findSqliteDll() == null
? 'SQLite-DLL fehlt (erst flutter build windows)'
: null;
test(
'UI-Testinhalte: DM pyramidtest2 → pyramidtest1 mit Text und Bildern',
() async {
final hs = Uri.parse(acc!['homeserver']!);
final c1 = await _login(
'AutopilotUiFixture1',
acc['user1']!,
acc['pass1']!,
hs,
);
final c2 = await _login(
'AutopilotUiFixture2',
acc['user2']!,
acc['pass2']!,
hs,
);
try {
await c1.oneShotSync().timeout(const Duration(seconds: 60));
await c2.oneShotSync().timeout(const Duration(seconds: 60));
final roomId = await c2.startDirectChat(
c1.userID!,
enableEncryption: false,
preset: CreateRoomPreset.privateChat,
);
final room = c2.getRoomById(roomId)!;
// user1 nimmt die Einladung an (falls noch nicht drin) – über
// room.join(), damit sie wie in der App als Direktnachricht zählt.
await c1.oneShotSync().timeout(const Duration(seconds: 60));
var r1 = c1.getRoomById(roomId);
if (r1 == null || r1.membership != Membership.join) {
if (r1 != null && r1.membership == Membership.invite) {
await r1.join();
} else {
await c1.joinRoom(roomId);
}
await c1
.waitForRoomInSync(roomId, join: true)
.timeout(const Duration(seconds: 60));
r1 = c1.getRoomById(roomId);
}
// „Chats“ in Pyramid zeigt nur Direktnachrichten (m.direct).
if (r1 != null && !r1.isDirectChat) {
await r1.addToDirectChat(c2.userID!);
}
final first = await room.sendTextEvent(
'Hallo! Das ist ein UI-Test mit einer etwas längeren Nachricht, damit '
'man sieht, wie Zeilenumbrüche, Schriftgröße und Abstände auf dem '
'Handy wirken. Umlaute: äöü ÄÖÜ ß.',
);
expect(first, isNotNull);
await c2.oneShotSync().timeout(const Duration(seconds: 30));
final firstEvent = await room.getEventById(first!);
await room.sendTextEvent(
'Kurze Antwort auf die erste Nachricht',
inReplyTo: firstEvent,
);
await room.sendFileEvent(
MatrixImageFile(
bytes: _testImage(720, 1280, img.ColorRgb8(59, 109, 17)),
name: 'hochkant.jpg',
mimeType: 'image/jpeg',
width: 720,
height: 1280,
),
);
await room.sendFileEvent(
MatrixImageFile(
bytes: _testImage(1280, 720, img.ColorRgb8(24, 95, 165)),
name: 'querformat.jpg',
mimeType: 'image/jpeg',
width: 1280,
height: 720,
),
);
await room.sendTextEvent('Und noch eine letzte Nachricht 👋');
// Eigene Nachrichten (aus Sicht von pyramidtest1 = rechts).
final myDm = c1.getRoomById(roomId)!;
await myDm.sendTextEvent('Danke, sieht gut aus!');
await myDm.sendTextEvent(
'Und eine etwas längere eigene Antwort, damit man sieht, wie '
'Sprechblasen umbrechen, wenn der Text über mehrere Zeilen geht.',
);
// Gruppenraum (Name + Avatar je Absender in der Sprechblasen-Ansicht).
const groupName = 'UI-Test Gruppe';
Room? group;
for (final r in c2.rooms) {
if (r.membership == Membership.join && r.name == groupName) {
group = r;
}
}
if (group == null) {
final groupId = await c2.createRoom(
name: groupName,
preset: CreateRoomPreset.privateChat,
invite: [c1.userID!],
);
await c2
.waitForRoomInSync(groupId, join: true)
.timeout(const Duration(seconds: 60));
group = c2.getRoomById(groupId)!;
await c1.oneShotSync().timeout(const Duration(seconds: 60));
await c1.getRoomById(groupId)?.join();
await c1
.waitForRoomInSync(groupId, join: true)
.timeout(const Duration(seconds: 60));
}
await group.sendTextEvent('Hallo zusammen! Wer ist am Samstag dabei?');
await group.sendTextEvent('Ich bringe Kuchen mit.');
await c1.getRoomById(group.id)!.sendTextEvent('Ich komme gern!');
// ignore: avoid_print
print('UI-Testraum bereit: $roomId, Gruppe: ${group.id}');
} finally {
await _logout(c1);
await _logout(c2);
await c1.dispose(closeDatabase: false);
await c2.dispose(closeDatabase: false);
}
},
skip: skip,
timeout: const Timeout(Duration(minutes: 5)),
);
}