@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? _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 = {}; 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 _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 _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 👋'); // ignore: avoid_print print('UI-Testraum bereit: $roomId'); } 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)), ); }