chore: restore product scaffold to rollback baseline

This commit is contained in:
Edison Jwa
2026-05-29 14:02:04 +09:00
parent 2896f14ec9
commit fe6e07353e
434 changed files with 27278 additions and 63230 deletions
@@ -31,14 +31,14 @@ void main() {
expect(state.agcEnabled, isTrue);
});
test('desktop VAD normalization keeps explicit WebRTC selections', () {
test('normalizes desktop VAD backend to Silero', () {
expect(
normalizedVadBackend(
rust.BridgeVadBackend.webrtcVad,
isWindows: true,
isLinux: false,
),
rust.BridgeVadBackend.webrtcVad,
rust.BridgeVadBackend.sileroOnnx,
);
expect(
normalizedVadBackend(
@@ -46,33 +46,10 @@ void main() {
isWindows: false,
isLinux: true,
),
rust.BridgeVadBackend.webrtcVad,
);
expect(
normalizedVadBackend(
rust.BridgeVadBackend.energyDebug,
isWindows: true,
isLinux: false,
),
rust.BridgeVadBackend.sileroOnnx,
);
});
test(
'desktop VAD normalization falls back when ONNX Runtime is unavailable',
() {
expect(
normalizedVadBackend(
rust.BridgeVadBackend.sileroOnnx,
isWindows: false,
isLinux: true,
onnxRuntimeAvailable: false,
),
rust.BridgeVadBackend.webrtcVad,
);
},
);
test('default config matches the current platform fallback', () {
final fallback = defaultAudioProcessingConfig();
final desktop = Platform.isWindows || Platform.isLinux;
@@ -126,7 +103,6 @@ void main() {
test('builds Windows/Linux software WebRTC APM config consistently', () {
final state = AudioProcessingConfigState.fromConfig(baseConfig)
..vadBackend = rust.BridgeVadBackend.webrtcVad
..nsEnabled = true
..aecEnabled = false
..agcEnabled = true;
@@ -151,7 +127,7 @@ void main() {
for (final config in [windowsConfig, linuxConfig]) {
expect(config.processingBackend, rust.BridgeAudioBackend.webrtcApm);
expect(config.vadBackend, rust.BridgeVadBackend.webrtcVad);
expect(config.vadBackend, rust.BridgeVadBackend.sileroOnnx);
expect(config.aec, rust.BridgeEffectOwner.off);
expect(config.ns, rust.BridgeEffectOwner.webrtcApm);
expect(config.agc, rust.BridgeEffectOwner.webrtcApm);
@@ -36,12 +36,10 @@ void main() {
required BigInt id,
required String name,
required BigInt channelId,
String uid = '',
bool isServerQuery = false,
}) {
return rust.BridgeClient(
id: id,
uid: uid,
channel: channelId,
name: name,
inputMuted: false,
@@ -466,6 +464,8 @@ void main() {
) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppL10n.localizationsDelegates,
supportedLocales: AppL10n.supportedLocales,
home: ChatPage(
messages: [
entry(rust.BridgeMessageTarget.poke(BigInt.from(2))),
@@ -498,15 +498,7 @@ void main() {
final privateTop = tester.getTopLeft(find.text('Alpha').first).dy;
final serverTileSize = tester.getSize(
find
.ancestor(
of: find.byIcon(Icons.dns_outlined),
matching: find.byWidgetPredicate(
(widget) =>
widget is SizedBox &&
widget.width == 92 &&
widget.height == 92,
),
)
.ancestor(of: find.text('Server').first, matching: find.byType(Ink))
.first,
);
@@ -516,63 +508,23 @@ void main() {
expect(serverTileSize.height, 92);
});
testWidgets('chat sidebar uses MD3-style selected container colors', (
testWidgets('chat page uses a compact top rail on phone width', (
tester,
) async {
final theme = ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
);
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: ChatPage(
messages: const [],
snapshot: snapshot(channels: const [], clients: const []),
initialTarget: const rust.BridgeMessageTarget.server(),
),
),
);
final serverIndicator = find.ancestor(
of: find.byIcon(Icons.dns_outlined),
matching: find.byType(AnimatedContainer),
);
final channelIndicator = find.ancestor(
of: find.byIcon(Icons.tag),
matching: find.byType(AnimatedContainer),
);
final serverDecoration =
tester.widget<AnimatedContainer>(serverIndicator).decoration
as BoxDecoration;
final channelDecoration =
tester.widget<AnimatedContainer>(channelIndicator).decoration
as BoxDecoration;
final serverLabel = tester.widget<Text>(find.text('Server').last);
final channelLabel = tester.widget<Text>(find.text('Channel'));
expect(tester.getSize(serverIndicator), const Size(76, 76));
expect(tester.getSize(channelIndicator), const Size(76, 76));
expect(serverDecoration.color, theme.colorScheme.primaryContainer);
expect(channelDecoration.color, Colors.transparent);
expect(serverLabel.style?.color, theme.colorScheme.onPrimaryContainer);
expect(channelLabel.style?.color, theme.colorScheme.onSurfaceVariant);
});
testWidgets('long private chat labels stay within fixed rail indicator', (
tester,
) async {
const longName = 'Very Long Private Chat Name That Should Not Stretch';
tester.view.physicalSize = const Size(390, 844);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppL10n.localizationsDelegates,
supportedLocales: AppL10n.supportedLocales,
home: ChatPage(
messages: [
ChatEntry(
senderId: BigInt.from(2),
senderName: longName,
senderName: 'Alpha',
message: 'Private',
target: rust.BridgeMessageTarget.client(BigInt.from(2)),
),
@@ -580,29 +532,24 @@ void main() {
snapshot: snapshot(
channels: const [],
clients: [
client(
id: BigInt.from(2),
name: longName,
channelId: BigInt.zero,
),
client(id: BigInt.from(2), name: 'Alpha', channelId: BigInt.zero),
],
),
initialTarget: rust.BridgeMessageTarget.client(BigInt.from(2)),
initialClientName: longName,
),
),
);
final selectedIndicator = find.ancestor(
of: find.text(longName).first,
matching: find.byType(AnimatedContainer),
);
final privateLabel = tester.widget<Text>(find.text(longName).first);
await tester.pumpAndSettle();
expect(tester.getSize(selectedIndicator), const Size(76, 76));
expect(privateLabel.maxLines, 2);
expect(privateLabel.overflow, TextOverflow.ellipsis);
expect(privateLabel.textAlign, TextAlign.center);
expect(find.byType(VerticalDivider), findsNothing);
final serverTileSize = tester.getSize(
find
.ancestor(of: find.text('Server').first, matching: find.byType(Ink))
.first,
);
expect(serverTileSize.width, lessThan(92));
expect(serverTileSize.height, lessThan(92));
expect(tester.getTopLeft(find.text('Private').first).dy, greaterThan(92));
});
testWidgets(
@@ -610,6 +557,8 @@ void main() {
(tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppL10n.localizationsDelegates,
supportedLocales: AppL10n.supportedLocales,
home: ChatPage(
messages: [
ChatEntry(
@@ -715,6 +664,8 @@ void main() {
) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppL10n.localizationsDelegates,
supportedLocales: AppL10n.supportedLocales,
home: ChatPage(
messages: [
ChatEntry(
@@ -755,6 +706,8 @@ void main() {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppL10n.localizationsDelegates,
supportedLocales: AppL10n.supportedLocales,
home: ChatPage(
messages: messages,
snapshot: currentSnapshot,
@@ -822,6 +775,8 @@ void main() {
Ts3ServerLink? tapped;
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppL10n.localizationsDelegates,
supportedLocales: AppL10n.supportedLocales,
home: ChatPage(
messages: [
ChatEntry(
@@ -0,0 +1,112 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:chanora_flutter/l10n/generated/app_localizations.dart';
import 'package:chanora_flutter/src/rust/api.dart' as rust;
import 'package:chanora_flutter/widgets/client_info_sheet.dart';
void main() {
rust.BridgeClientProfile profile() {
return rust.BridgeClientProfile(
id: BigInt.from(101),
channel: BigInt.from(7),
name: 'Bob',
uniqueId: 'client-unique-id',
databaseId: BigInt.from(55),
countryCode: 'US',
description: 'Operator',
version: '3.6.2',
platform: 'Windows',
onlineSeconds: 3661,
idleMilliseconds: 42000,
pingMilliseconds: 38,
clientAddress: '203.0.113.24',
serverGroups: const ['Admin', 'Talk Power'],
channelGroup: 'Guest',
avatarPath: '/avatar_aabbcc',
bytesDownloadedMonth: BigInt.from(2048),
bytesUploadedMonth: BigInt.from(4096),
bytesDownloadedTotal: BigInt.from(1048576),
bytesUploadedTotal: BigInt.from(2097152),
packetLossClientToServerTotal: 0.0123,
packetLossServerToClientTotal: 0.0456,
);
}
testWidgets('renders loaded profile data in a modal sheet', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppL10n.localizationsDelegates,
supportedLocales: AppL10n.supportedLocales,
home: Builder(
builder: (context) => Scaffold(
body: Center(
child: FilledButton(
onPressed: () => showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
useSafeArea: true,
builder: (context) => FractionallySizedBox(
heightFactor: 0.86,
child: ClientInfoSheet(
clientName: 'Bob',
loadProfile: () async => profile(),
),
),
),
child: const Text('Open'),
),
),
),
),
),
);
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
expect(find.text('Bob'), findsOneWidget);
expect(find.text('Identity'), findsOneWidget);
expect(find.text('Membership'), findsOneWidget);
expect(find.text('Admin, Talk Power'), findsOneWidget);
await tester.drag(find.byType(ListView), const Offset(0, -360));
await tester.pumpAndSettle();
expect(find.text('Connection'), findsOneWidget);
expect(find.text('1h 1m 1s'), findsOneWidget);
expect(find.text('42.00 s'), findsOneWidget);
expect(find.text('203.0.113.24'), findsOneWidget);
});
testWidgets('loading and error states use localized copy', (tester) async {
await tester.pumpWidget(
MaterialApp(
locale: const Locale('zh'),
localizationsDelegates: AppL10n.localizationsDelegates,
supportedLocales: AppL10n.supportedLocales,
home: Scaffold(
body: SizedBox(
height: 500,
child: ClientInfoSheet(
clientName: 'Bob',
loadProfile: () => Future<rust.BridgeClientProfile>.error(
StateError('not available'),
),
),
),
),
),
);
expect(find.text('正在加载资料…'), findsOneWidget);
expect(find.text('Loading profile...'), findsNothing);
await tester.pumpAndSettle();
expect(find.text('资料不可用'), findsOneWidget);
expect(find.text('重试'), findsOneWidget);
expect(find.text('Profile unavailable'), findsNothing);
});
}
@@ -1,41 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:chanora_flutter/l10n/generated/app_localizations.dart';
import 'package:chanora_flutter/widgets/connect_widgets.dart';
void main() {
testWidgets('pressing enter in password field submits connection', (
tester,
) async {
var connectCount = 0;
final hostCtl = TextEditingController(text: 'example.com');
final nickCtl = TextEditingController(text: 'alice');
final passwordCtl = TextEditingController();
addTearDown(hostCtl.dispose);
addTearDown(nickCtl.dispose);
addTearDown(passwordCtl.dispose);
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppL10n.localizationsDelegates,
supportedLocales: AppL10n.supportedLocales,
home: Scaffold(
body: ConnectForm(
hostCtl: hostCtl,
nickCtl: nickCtl,
passwordCtl: passwordCtl,
onConnect: () => connectCount += 1,
onAddBookmark: () {},
),
),
),
);
await tester.enterText(find.byType(TextField).last, 'secret');
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pump();
expect(connectCount, 1);
});
}
@@ -1,41 +0,0 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:chanora_flutter/widgets/input_dialogs.dart';
void main() {
test('browser back and forward logical keys map to mouse side bindings', () {
expect(
pttMouseSideButtonPlatformKeyForLogicalKey(
LogicalKeyboardKey.browserBack,
),
'mouse-side-button:8',
);
expect(
pttMouseSideButtonPlatformKeyForLogicalKey(LogicalKeyboardKey.goBack),
'mouse-side-button:8',
);
expect(
pttMouseSideButtonPlatformKeyForLogicalKey(
LogicalKeyboardKey.browserForward,
),
'mouse-side-button:16',
);
});
test('pointer button bitmasks map to mouse side bindings', () {
expect(
pttMouseSideButtonPlatformKeyForButtons(0x08),
'mouse-side-button:8',
);
expect(
pttMouseSideButtonPlatformKeyForButtons(0x10),
'mouse-side-button:16',
);
expect(
pttMouseSideButtonPlatformKeyForButtons(0x18),
'mouse-side-button:8',
);
expect(pttMouseSideButtonPlatformKeyForButtons(0x00), isNull);
});
}
@@ -0,0 +1,160 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:chanora_flutter/l10n/generated/app_localizations.dart';
import 'package:chanora_flutter/main.dart';
import 'package:chanora_flutter/widgets/connect_widgets.dart';
import 'package:chanora_flutter/widgets/input_dialogs.dart';
import 'package:chanora_flutter/widgets/ptt_capability_badge.dart';
void main() {
Widget localizedHarness(Widget child, {Locale? locale}) {
return MaterialApp(
locale: locale,
localizationsDelegates: AppL10n.localizationsDelegates,
supportedLocales: AppL10n.supportedLocales,
home: Scaffold(body: child),
);
}
testWidgets('connect actions stack on narrow mobile widths', (tester) async {
await tester.pumpWidget(
localizedHarness(
SizedBox(
width: 320,
child: ConnectForm(
hostCtl: TextEditingController(text: 'server.example.com'),
nickCtl: TextEditingController(text: 'MobileUser'),
passwordCtl: TextEditingController(),
onConnect: () {},
onAddBookmark: () {},
),
),
),
);
await tester.pumpAndSettle();
final connectTop = tester.getTopLeft(find.text('Connect')).dy;
final bookmarkTop = tester.getTopLeft(find.text('Save bookmark')).dy;
expect(bookmarkTop, greaterThan(connectTop + 44));
});
testWidgets('connect actions stack at standard phone content widths', (
tester,
) async {
await tester.pumpWidget(
localizedHarness(
SizedBox(
width: 361,
child: ConnectForm(
hostCtl: TextEditingController(text: 'server.example.com'),
nickCtl: TextEditingController(text: 'MobileUser'),
passwordCtl: TextEditingController(),
onConnect: () {},
onAddBookmark: () {},
),
),
),
);
await tester.pumpAndSettle();
final connectTop = tester.getTopLeft(find.text('Connect')).dy;
final bookmarkTop = tester.getTopLeft(find.text('Save bookmark')).dy;
expect(bookmarkTop, greaterThan(connectTop + 44));
});
testWidgets('disconnected mobile chrome keeps content near the safe area', (
tester,
) async {
const primaryContentKey = Key('primary-content');
await tester.binding.setSurfaceSize(const Size(393, 852));
addTearDown(() => tester.binding.setSurfaceSize(null));
await tester.pumpWidget(
localizedHarness(
ChanoraMobileScaffold(
compactIdleChrome: true,
canDisconnect: false,
title: null,
actions: [
IconButton(
tooltip: 'About',
icon: const Icon(Icons.info_outline),
onPressed: () {},
),
],
onDisconnect: () {},
compactHeader: const SizedBox(height: 44, child: Text('Chanora')),
body: const KeyedSubtree(
key: primaryContentKey,
child: Text('Primary content'),
),
),
),
);
await tester.pumpAndSettle();
expect(find.byType(AppBar), findsNothing);
expect(tester.getTopLeft(find.byKey(primaryContentKey)).dy, lessThan(120));
});
testWidgets('PTT explanation sheet is scrollable for mobile text scaling', (
tester,
) async {
await tester.pumpWidget(
localizedHarness(
MediaQuery(
data: const MediaQueryData(textScaler: TextScaler.linear(1.8)),
child: const SizedBox(
width: 320,
child: PttCapabilityBadge(
level: 'L0Focused',
backendId: 'focused',
boundInputClass: 'keyboard',
),
),
),
),
);
await tester.tap(find.byIcon(Icons.info_outline));
await tester.pumpAndSettle();
expect(find.byType(SingleChildScrollView), findsWidgets);
expect(tester.takeException(), isNull);
});
testWidgets('PTT capture dialog wraps content for small screens', (
tester,
) async {
await tester.pumpWidget(
localizedHarness(
Builder(
builder: (context) {
return Center(
child: FilledButton(
onPressed: () => showDialog<void>(
context: context,
builder: (_) => const PttBindingCaptureDialog(),
),
child: const Text('Open'),
),
);
},
),
),
);
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
expect(find.byType(SingleChildScrollView), findsOneWidget);
expect(tester.takeException(), isNull);
});
}
@@ -1,9 +1,8 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:chanora_flutter/l10n/generated/app_localizations.dart';
import 'package:chanora_flutter/services/ui_preferences_service.dart';
import 'package:chanora_flutter/src/rust/api.dart' as rust;
import 'package:chanora_flutter/widgets/snapshot_view.dart';
@@ -30,14 +29,12 @@ void main() {
required int id,
required int channelId,
required String name,
String uid = '',
bool speaking = false,
int talkPower = 0,
bool talkPowerGranted = false,
}) {
return rust.BridgeClient(
id: BigInt.from(id),
uid: uid,
channel: BigInt.from(channelId),
name: name,
inputMuted: false,
@@ -52,11 +49,14 @@ void main() {
Widget snapshotHarness({
required List<rust.BridgeChannel> channels,
required List<rust.BridgeClient> clients,
String welcomeMessage = '',
BigInt? ownClientId,
BigInt? currentVoiceChannelId,
rust.BridgeAudioStats? audioStats,
Map<String, ClientPlaybackPreference> clientPlaybackPreferences = const {},
ClientPlaybackPreferenceChanged? onClientPlaybackPreferenceChanged,
bool enableClientLongPressMenu = false,
ValueChanged<rust.BridgeClient>? onOpenClientInfo,
ValueChanged<rust.BridgeClient>? onOpenClientChat,
ValueChanged<rust.BridgeClient>? onOpenClientPoke,
}) {
return MaterialApp(
localizationsDelegates: AppL10n.localizationsDelegates,
@@ -65,7 +65,7 @@ void main() {
body: SnapshotView(
snapshot: rust.BridgeSnapshot(
serverName: 'Server',
welcomeMessage: '',
welcomeMessage: welcomeMessage,
platform: '',
version: '',
channels: channels,
@@ -81,8 +81,10 @@ void main() {
canJoinVoiceChannel: true,
onJoinChannel: (_) {},
onJoinChannelWithPassword: (_) {},
clientPlaybackPreferences: clientPlaybackPreferences,
onClientPlaybackPreferenceChanged: onClientPlaybackPreferenceChanged,
enableClientLongPressMenu: enableClientLongPressMenu,
onOpenClientInfo: onOpenClientInfo,
onOpenClientChat: onOpenClientChat,
onOpenClientPoke: onOpenClientPoke,
),
),
);
@@ -197,6 +199,27 @@ void main() {
expect(childUserX - childX, inInclusiveRange(22, 28));
});
testWidgets('server welcome starts collapsed above channel tree', (
tester,
) async {
await tester.pumpWidget(
snapshotHarness(
welcomeMessage:
'This is a long server welcome that should not push channels '
'below the first connected screen.',
channels: [channel(id: 1, name: 'Default Channel')],
clients: [client(id: 100, channelId: 1, name: 'Alice')],
),
);
await tester.pumpAndSettle();
expect(find.text('Server welcome message'), findsOneWidget);
expect(find.textContaining('long server welcome'), findsNothing);
expect(find.text('Default Channel'), findsOneWidget);
expect(find.text('Alice'), findsOneWidget);
});
testWidgets('password channel shows lock at row end', (tester) async {
await tester.pumpWidget(
snapshotHarness(
@@ -242,6 +265,219 @@ void main() {
expect(userHighlight.decoration, isNull);
});
testWidgets('long press opens client menu for other users', (tester) async {
rust.BridgeClient? chatClient;
rust.BridgeClient? pokeClient;
await tester.pumpWidget(
snapshotHarness(
channels: [channel(id: 1, name: 'Default Channel')],
clients: [
client(id: 100, channelId: 1, name: 'Alice'),
client(id: 101, channelId: 1, name: 'Bob'),
],
ownClientId: BigInt.from(100),
enableClientLongPressMenu: true,
onOpenClientChat: (client) => chatClient = client,
onOpenClientPoke: (client) => pokeClient = client,
),
);
await tester.pumpAndSettle();
await tester.longPress(find.text('Bob'));
await tester.pumpAndSettle();
expect(find.text('Private message'), findsOneWidget);
expect(find.text('Poke'), findsOneWidget);
await tester.tap(find.text('Private message'));
await tester.pumpAndSettle();
expect(chatClient?.id, BigInt.from(101));
expect(chatClient?.name, 'Bob');
expect(pokeClient, isNull);
});
testWidgets('long press is ignored when mobile client menus are disabled', (
tester,
) async {
await tester.pumpWidget(
snapshotHarness(
channels: [channel(id: 1, name: 'Default Channel')],
clients: [
client(id: 100, channelId: 1, name: 'Alice'),
client(id: 101, channelId: 1, name: 'Bob'),
],
ownClientId: BigInt.from(100),
enableClientLongPressMenu: false,
onOpenClientChat: (_) {},
onOpenClientPoke: (_) {},
),
);
await tester.pumpAndSettle();
await tester.longPress(find.text('Bob'));
await tester.pumpAndSettle();
expect(find.text('Private message'), findsNothing);
expect(find.text('Poke'), findsNothing);
});
testWidgets('secondary click opens client menu for other users', (
tester,
) async {
rust.BridgeClient? chatClient;
await tester.pumpWidget(
snapshotHarness(
channels: [channel(id: 1, name: 'Default Channel')],
clients: [
client(id: 100, channelId: 1, name: 'Alice'),
client(id: 101, channelId: 1, name: 'Bob'),
],
ownClientId: BigInt.from(100),
onOpenClientChat: (client) => chatClient = client,
),
);
await tester.pumpAndSettle();
await tester.tap(
find.text('Bob'),
buttons: kSecondaryMouseButton,
kind: PointerDeviceKind.mouse,
);
await tester.pumpAndSettle();
expect(find.text('Private message'), findsOneWidget);
await tester.tap(find.text('Private message'));
await tester.pumpAndSettle();
expect(chatClient?.id, BigInt.from(101));
});
testWidgets('client menu poke action targets selected other user', (
tester,
) async {
rust.BridgeClient? pokeClient;
await tester.pumpWidget(
snapshotHarness(
channels: [channel(id: 1, name: 'Default Channel')],
clients: [
client(id: 100, channelId: 1, name: 'Alice'),
client(id: 101, channelId: 1, name: 'Bob'),
],
ownClientId: BigInt.from(100),
enableClientLongPressMenu: true,
onOpenClientPoke: (client) => pokeClient = client,
),
);
await tester.pumpAndSettle();
await tester.longPress(find.text('Bob'));
await tester.pumpAndSettle();
await tester.tap(find.text('Poke'));
await tester.pumpAndSettle();
expect(pokeClient?.id, BigInt.from(101));
expect(pokeClient?.name, 'Bob');
expect(find.text('Poke'), findsNothing);
});
testWidgets('client menu info action targets selected other user', (
tester,
) async {
rust.BridgeClient? infoClient;
await tester.pumpWidget(
snapshotHarness(
channels: [channel(id: 1, name: 'Default Channel')],
clients: [
client(id: 100, channelId: 1, name: 'Alice'),
client(id: 101, channelId: 1, name: 'Bob'),
],
ownClientId: BigInt.from(100),
enableClientLongPressMenu: true,
onOpenClientInfo: (client) => infoClient = client,
),
);
await tester.pumpAndSettle();
await tester.longPress(find.text('Bob'));
await tester.pumpAndSettle();
expect(find.text('Info'), findsOneWidget);
await tester.tap(find.text('Info'));
await tester.pumpAndSettle();
expect(infoClient?.id, BigInt.from(101));
expect(infoClient?.name, 'Bob');
});
testWidgets('client menu info action is available for self', (tester) async {
rust.BridgeClient? infoClient;
rust.BridgeClient? chatClient;
rust.BridgeClient? pokeClient;
await tester.pumpWidget(
snapshotHarness(
channels: [channel(id: 1, name: 'Default Channel')],
clients: [
client(id: 100, channelId: 1, name: 'Alice'),
client(id: 101, channelId: 1, name: 'Bob'),
],
ownClientId: BigInt.from(100),
enableClientLongPressMenu: true,
onOpenClientInfo: (client) => infoClient = client,
onOpenClientChat: (client) => chatClient = client,
onOpenClientPoke: (client) => pokeClient = client,
),
);
await tester.pumpAndSettle();
await tester.longPress(find.text('Alice'));
await tester.pumpAndSettle();
expect(find.text('Info'), findsOneWidget);
expect(find.text('Private message'), findsNothing);
expect(find.text('Poke'), findsNothing);
await tester.tap(find.text('Info'));
await tester.pumpAndSettle();
expect(infoClient?.id, BigInt.from(100));
expect(infoClient?.name, 'Alice');
expect(chatClient, isNull);
expect(pokeClient, isNull);
});
testWidgets('self menu is hidden when only peer actions are available', (
tester,
) async {
await tester.pumpWidget(
snapshotHarness(
channels: [channel(id: 1, name: 'Default Channel')],
clients: [
client(id: 100, channelId: 1, name: 'Alice'),
client(id: 101, channelId: 1, name: 'Bob'),
],
ownClientId: BigInt.from(100),
enableClientLongPressMenu: true,
onOpenClientChat: (_) {},
onOpenClientPoke: (_) {},
),
);
await tester.pumpAndSettle();
await tester.longPress(find.text('Alice'));
await tester.pumpAndSettle();
expect(find.text('Private message'), findsNothing);
expect(find.text('Poke'), findsNothing);
});
testWidgets(
'local voice activity does not light speaking state when blocked',
(tester) async {
@@ -270,114 +506,6 @@ void main() {
},
);
testWidgets('remote client row does not show playback ellipsis', (
tester,
) async {
await tester.pumpWidget(
snapshotHarness(
channels: [channel(id: 1, name: 'Default Channel')],
clients: [
client(id: 100, channelId: 1, name: 'Self', uid: 'self'),
client(id: 101, channelId: 1, name: 'Bob', uid: 'user-b'),
],
ownClientId: BigInt.from(100),
clientPlaybackPreferences: const {
'user-b': ClientPlaybackPreference(volume: 0.5, muted: false),
},
onClientPlaybackPreferenceChanged: (_, _) async {},
),
);
await tester.pumpAndSettle();
expect(find.byIcon(Icons.more_horiz), findsNothing);
});
testWidgets('remote client long press opens playback menu', (tester) async {
await tester.pumpWidget(
snapshotHarness(
channels: [channel(id: 1, name: 'Default Channel')],
clients: [
client(id: 100, channelId: 1, name: 'Self', uid: 'self'),
client(id: 101, channelId: 1, name: 'Bob', uid: 'user-b'),
],
ownClientId: BigInt.from(100),
onClientPlaybackPreferenceChanged: (_, _) async {},
),
);
await tester.pumpAndSettle();
await tester.longPress(find.widgetWithText(ListTile, 'Bob'));
await tester.pumpAndSettle();
expect(find.text('Mute playback'), findsOneWidget);
expect(find.text('Volume 100%'), findsOneWidget);
});
testWidgets('remote client secondary click opens playback menu', (
tester,
) async {
await tester.pumpWidget(
snapshotHarness(
channels: [channel(id: 1, name: 'Default Channel')],
clients: [
client(id: 100, channelId: 1, name: 'Self', uid: 'self'),
client(id: 101, channelId: 1, name: 'Bob', uid: 'user-b'),
],
ownClientId: BigInt.from(100),
onClientPlaybackPreferenceChanged: (_, _) async {},
),
);
await tester.pumpAndSettle();
await tester.tap(
find.widgetWithText(ListTile, 'Bob'),
buttons: kSecondaryButton,
);
await tester.pumpAndSettle();
expect(find.text('Mute playback'), findsOneWidget);
expect(find.text('Volume 100%'), findsOneWidget);
});
testWidgets('client playback menu forwards mute and volume changes', (
tester,
) async {
final changes = <ClientPlaybackPreference>[];
await tester.pumpWidget(
snapshotHarness(
channels: [channel(id: 1, name: 'Default Channel')],
clients: [
client(id: 100, channelId: 1, name: 'Self', uid: 'self'),
client(id: 101, channelId: 1, name: 'Bob', uid: 'user-b'),
],
ownClientId: BigInt.from(100),
onClientPlaybackPreferenceChanged: (_, preference) async {
changes.add(preference);
},
),
);
await tester.pumpAndSettle();
await tester.longPress(find.widgetWithText(ListTile, 'Bob'));
await tester.pumpAndSettle();
await tester.tap(find.text('Mute playback'));
await tester.pumpAndSettle();
expect(changes, isNotEmpty);
expect(changes.last.muted, isTrue);
await tester.longPress(find.widgetWithText(ListTile, 'Bob'));
await tester.pumpAndSettle();
await tester.tap(find.text('Volume 200%'), warnIfMissed: false);
await tester.pumpAndSettle();
expect(changes.last.volume, 2.0);
expect(changes.last.muted, isFalse);
});
testWidgets('spacer channels render as layout rows and keep channel taps', (
tester,
) async {
@@ -1,67 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:chanora_flutter/services/startup_dependency_check.dart';
import 'package:chanora_flutter/widgets/startup_dependency_screen.dart';
void main() {
testWidgets('startup gate shows install-help screen and can continue', (
tester,
) async {
tester.view.physicalSize = const Size(1200, 1800);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final result = StartupDependencyCheckResult(
platformLabel: 'Fedora',
issues: const [
StartupDependencyIssue(
id: 'linux-pipewire-runtime',
title: 'PipeWire runtime is missing',
summary: 'Primary Linux audio needs PipeWire.',
details: ['Install PipeWire and recheck.'],
severity: StartupDependencySeverity.recommended,
installHints: [
StartupInstallHint(
label: 'Release downloads',
command: 'https://github.com/microsoft/onnxruntime/releases',
),
StartupInstallHint(
label: 'Fedora',
command: 'sudo dnf install pipewire-libs',
),
],
),
],
);
final loggedResults = <StartupDependencyCheckResult>[];
await tester.pumpWidget(
MaterialApp(
home: StartupDependencyGate(
checker: () async => result,
logger: (value) async {
loggedResults.add(value);
},
child: const Scaffold(body: Text('ready')),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Finish Linux setup'), findsOneWidget);
expect(find.text('PipeWire runtime is missing'), findsOneWidget);
expect(find.text('Continue anyway'), findsOneWidget);
expect(find.text('Open'), findsOneWidget);
expect(find.text('Copy'), findsOneWidget);
expect(loggedResults, [result]);
await tester.ensureVisible(find.text('Continue anyway'));
await tester.tap(find.text('Continue anyway'));
await tester.pumpAndSettle();
expect(find.text('ready'), findsOneWidget);
});
}
@@ -24,29 +24,12 @@ void main() {
]);
});
test('desktop VAD segments expose both Silero and WebRTC', () {
test('desktop VAD segments expose Silero as the primary backend', () {
expect(desktopVadBackendSegments.map((s) => s.value), [
rust.BridgeVadBackend.webrtcVad,
rust.BridgeVadBackend.sileroOnnx,
]);
});
test('desktop VAD segments disable Silero when ONNX Runtime is missing', () {
final segments = vadBackendSegmentsForAvailability(
desktop: true,
onnxRuntimeAvailable: false,
);
final silero = segments.singleWhere(
(segment) => segment.value == rust.BridgeVadBackend.sileroOnnx,
);
final webrtc = segments.singleWhere(
(segment) => segment.value == rust.BridgeVadBackend.webrtcVad,
);
expect(silero.enabled, isFalse);
expect(webrtc.enabled, isTrue);
});
testWidgets('shared segmented style applies compact visual density', (
tester,
) async {