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]>
255 lines
8.3 KiB
Dart
255 lines
8.3 KiB
Dart
@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)),
|
||
);
|
||
}
|