Handle pokes outside chat tabs
This commit is contained in:
@@ -0,0 +1,470 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:chanora_flutter/l10n/generated/app_localizations.dart';
|
||||
import 'package:chanora_flutter/services/ts3_server_link.dart';
|
||||
import 'package:chanora_flutter/src/rust/api.dart' as rust;
|
||||
import 'package:chanora_flutter/widgets/chat_views.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
ChatEntry entry(rust.BridgeMessageTarget target) {
|
||||
return ChatEntry(
|
||||
senderId: BigInt.one,
|
||||
senderName: 'Sender',
|
||||
message: 'Message',
|
||||
target: target,
|
||||
);
|
||||
}
|
||||
|
||||
rust.BridgeChannel channel(BigInt id, String name) {
|
||||
return rust.BridgeChannel(
|
||||
id: id,
|
||||
parent: BigInt.zero,
|
||||
name: name,
|
||||
order: 0,
|
||||
hasPassword: false,
|
||||
neededTalkPower: 0,
|
||||
);
|
||||
}
|
||||
|
||||
rust.BridgeClient client({
|
||||
required BigInt id,
|
||||
required String name,
|
||||
required BigInt channelId,
|
||||
bool isServerQuery = false,
|
||||
}) {
|
||||
return rust.BridgeClient(
|
||||
id: id,
|
||||
channel: channelId,
|
||||
name: name,
|
||||
inputMuted: false,
|
||||
outputMuted: false,
|
||||
isSpeaking: false,
|
||||
isServerQuery: isServerQuery,
|
||||
talkPower: 0,
|
||||
talkPowerGranted: false,
|
||||
);
|
||||
}
|
||||
|
||||
rust.BridgeSnapshot snapshot({
|
||||
required List<rust.BridgeChannel> channels,
|
||||
required List<rust.BridgeClient> clients,
|
||||
}) {
|
||||
return rust.BridgeSnapshot(
|
||||
serverName: 'Server',
|
||||
welcomeMessage: '',
|
||||
platform: '',
|
||||
version: '',
|
||||
channels: channels,
|
||||
clients: clients,
|
||||
ownClientId: BigInt.one,
|
||||
);
|
||||
}
|
||||
|
||||
test('initial chat target opens latest private conversation first', () {
|
||||
final target = resolveInitialChatTarget(
|
||||
messages: [
|
||||
entry(rust.BridgeMessageTarget.client(BigInt.from(1))),
|
||||
entry(rust.BridgeMessageTarget.poke(BigInt.from(2))),
|
||||
entry(rust.BridgeMessageTarget.client(BigInt.from(3))),
|
||||
],
|
||||
currentVoiceChannelId: BigInt.from(9),
|
||||
);
|
||||
|
||||
expect(target, rust.BridgeMessageTarget.client(BigInt.from(3)));
|
||||
});
|
||||
|
||||
test('initial chat target ignores pokes and falls back to channel', () {
|
||||
final target = resolveInitialChatTarget(
|
||||
messages: [
|
||||
entry(const rust.BridgeMessageTarget.server()),
|
||||
entry(rust.BridgeMessageTarget.poke(BigInt.from(2))),
|
||||
entry(rust.BridgeMessageTarget.poke(BigInt.from(4))),
|
||||
],
|
||||
currentVoiceChannelId: BigInt.from(9),
|
||||
);
|
||||
|
||||
expect(target, const rust.BridgeMessageTarget.channel());
|
||||
});
|
||||
|
||||
test('initial chat target falls back to channel when connected to voice', () {
|
||||
final target = resolveInitialChatTarget(
|
||||
messages: [
|
||||
entry(const rust.BridgeMessageTarget.server()),
|
||||
entry(const rust.BridgeMessageTarget.channel()),
|
||||
],
|
||||
currentVoiceChannelId: BigInt.from(9),
|
||||
);
|
||||
|
||||
expect(target, const rust.BridgeMessageTarget.channel());
|
||||
});
|
||||
|
||||
test('initial chat target falls back to hub when nothing is active', () {
|
||||
final target = resolveInitialChatTarget(
|
||||
messages: const [],
|
||||
currentVoiceChannelId: null,
|
||||
);
|
||||
|
||||
expect(target, isNull);
|
||||
});
|
||||
|
||||
test(
|
||||
'groups chat picker clients by channel and excludes own/query clients',
|
||||
() {
|
||||
final groups = groupChatClientsForPicker(
|
||||
snapshot(
|
||||
channels: [channel(BigInt.from(10), 'Lobby')],
|
||||
clients: [
|
||||
client(id: BigInt.one, name: 'Me', channelId: BigInt.from(10)),
|
||||
client(
|
||||
id: BigInt.from(2),
|
||||
name: 'Zulu',
|
||||
channelId: BigInt.from(10),
|
||||
),
|
||||
client(
|
||||
id: BigInt.from(3),
|
||||
name: 'Alpha',
|
||||
channelId: BigInt.from(10),
|
||||
),
|
||||
client(
|
||||
id: BigInt.from(4),
|
||||
name: 'Query',
|
||||
channelId: BigInt.from(10),
|
||||
isServerQuery: true,
|
||||
),
|
||||
client(
|
||||
id: BigInt.from(5),
|
||||
name: 'Orphan',
|
||||
channelId: BigInt.from(99),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
groups.clientsByChannel[BigInt.from(10)]?.map((client) => client.name),
|
||||
['Alpha', 'Zulu'],
|
||||
);
|
||||
expect(groups.ungroupedClients.map((client) => client.name), ['Orphan']);
|
||||
},
|
||||
);
|
||||
|
||||
test('filters chat picker groups by client name case-insensitively', () {
|
||||
final groups = filterChatClientPickerGroups(
|
||||
channels: [
|
||||
channel(BigInt.from(10), 'Lobby'),
|
||||
channel(BigInt.from(20), 'Games'),
|
||||
],
|
||||
clientsByChannel: {
|
||||
BigInt.from(10): [
|
||||
client(id: BigInt.from(2), name: 'Alpha', channelId: BigInt.from(10)),
|
||||
],
|
||||
BigInt.from(20): [
|
||||
client(id: BigInt.from(3), name: 'Bravo', channelId: BigInt.from(20)),
|
||||
],
|
||||
},
|
||||
ungroupedClients: [
|
||||
client(id: BigInt.from(4), name: 'Alfred', channelId: BigInt.from(99)),
|
||||
],
|
||||
query: 'AL',
|
||||
);
|
||||
|
||||
expect(groups.map((group) => group.channelName), ['Lobby', 'Other']);
|
||||
expect(
|
||||
groups.expand((group) => group.clients).map((client) => client.name),
|
||||
['Alpha', 'Alfred'],
|
||||
);
|
||||
});
|
||||
|
||||
test('keeps all chat picker groups for blank queries', () {
|
||||
final groups = filterChatClientPickerGroups(
|
||||
channels: [channel(BigInt.from(10), 'Lobby')],
|
||||
clientsByChannel: {
|
||||
BigInt.from(10): [
|
||||
client(id: BigInt.from(2), name: 'Alpha', channelId: BigInt.from(10)),
|
||||
client(id: BigInt.from(3), name: 'Bravo', channelId: BigInt.from(10)),
|
||||
],
|
||||
},
|
||||
ungroupedClients: [
|
||||
client(id: BigInt.from(4), name: 'Orphan', channelId: BigInt.from(99)),
|
||||
],
|
||||
query: ' ',
|
||||
);
|
||||
|
||||
expect(groups.map((group) => group.channelName), ['Lobby', 'Other']);
|
||||
expect(groups.first.clients.map((client) => client.name), [
|
||||
'Alpha',
|
||||
'Bravo',
|
||||
]);
|
||||
expect(groups.last.clients.map((client) => client.name), ['Orphan']);
|
||||
});
|
||||
|
||||
test('omits empty chat picker groups after filtering', () {
|
||||
final groups = filterChatClientPickerGroups(
|
||||
channels: [
|
||||
channel(BigInt.from(10), 'Lobby'),
|
||||
channel(BigInt.from(20), 'Games'),
|
||||
],
|
||||
clientsByChannel: {
|
||||
BigInt.from(10): [
|
||||
client(id: BigInt.from(2), name: 'Alpha', channelId: BigInt.from(10)),
|
||||
],
|
||||
BigInt.from(20): [
|
||||
client(id: BigInt.from(3), name: 'Bravo', channelId: BigInt.from(20)),
|
||||
],
|
||||
},
|
||||
ungroupedClients: [
|
||||
client(id: BigInt.from(4), name: 'Orphan', channelId: BigInt.from(99)),
|
||||
],
|
||||
query: 'brav',
|
||||
);
|
||||
|
||||
expect(groups, hasLength(1));
|
||||
expect(groups.single.channelName, 'Games');
|
||||
expect(groups.single.clients.single.name, 'Bravo');
|
||||
});
|
||||
|
||||
test('formats spacer channel names for filtered chat picker groups', () {
|
||||
final groups = filterChatClientPickerGroups(
|
||||
channels: [channel(BigInt.from(10), '[cSpacer]Lobby')],
|
||||
clientsByChannel: {
|
||||
BigInt.from(10): [
|
||||
client(id: BigInt.from(2), name: 'Alpha', channelId: BigInt.from(10)),
|
||||
],
|
||||
},
|
||||
ungroupedClients: const [],
|
||||
query: '',
|
||||
);
|
||||
|
||||
expect(groups.single.channelName, 'Lobby');
|
||||
});
|
||||
|
||||
test('formats chat target titles', () {
|
||||
expect(
|
||||
chatTargetTitle(
|
||||
const rust.BridgeMessageTarget.server(),
|
||||
channelName: '',
|
||||
clientName: '',
|
||||
),
|
||||
'Server Activity',
|
||||
);
|
||||
expect(
|
||||
chatTargetTitle(
|
||||
const rust.BridgeMessageTarget.channel(),
|
||||
channelName: 'Lobby',
|
||||
clientName: '',
|
||||
),
|
||||
'# Lobby',
|
||||
);
|
||||
expect(
|
||||
chatTargetTitle(
|
||||
rust.BridgeMessageTarget.client(BigInt.from(2)),
|
||||
channelName: '',
|
||||
clientName: 'Alpha',
|
||||
),
|
||||
'Alpha',
|
||||
);
|
||||
expect(
|
||||
chatTargetTitle(
|
||||
rust.BridgeMessageTarget.poke(BigInt.from(2)),
|
||||
channelName: '',
|
||||
clientName: 'Alpha',
|
||||
),
|
||||
'Poke: Alpha',
|
||||
);
|
||||
});
|
||||
|
||||
test('formats chat empty-state text', () {
|
||||
expect(
|
||||
chatEmptyTitle(const rust.BridgeMessageTarget.server()),
|
||||
'No server activity yet',
|
||||
);
|
||||
expect(
|
||||
chatEmptyBody(
|
||||
const rust.BridgeMessageTarget.channel(),
|
||||
channelName: 'Lobby',
|
||||
),
|
||||
'Start the conversation in #Lobby.',
|
||||
);
|
||||
expect(
|
||||
chatEmptyBody(const rust.BridgeMessageTarget.channel(), channelName: ''),
|
||||
'Join a channel to start chatting.',
|
||||
);
|
||||
expect(
|
||||
chatEmptyBody(
|
||||
rust.BridgeMessageTarget.client(BigInt.from(2)),
|
||||
channelName: '',
|
||||
),
|
||||
'Select a user to start a private chat.',
|
||||
);
|
||||
});
|
||||
|
||||
test('formats chat input placeholders', () {
|
||||
expect(
|
||||
chatInputPlaceholder(
|
||||
const rust.BridgeMessageTarget.channel(),
|
||||
channelName: 'Lobby',
|
||||
clientName: '',
|
||||
),
|
||||
'Message #Lobby...',
|
||||
);
|
||||
expect(
|
||||
chatInputPlaceholder(
|
||||
rust.BridgeMessageTarget.client(BigInt.from(2)),
|
||||
channelName: '',
|
||||
clientName: 'Alpha',
|
||||
),
|
||||
'Message Alpha...',
|
||||
);
|
||||
expect(
|
||||
chatInputPlaceholder(
|
||||
rust.BridgeMessageTarget.poke(BigInt.from(2)),
|
||||
channelName: '',
|
||||
clientName: 'Alpha',
|
||||
),
|
||||
'Poke message...',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('chat hub does not expose pokes as a chat tab', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ChatPage(
|
||||
messages: [
|
||||
entry(rust.BridgeMessageTarget.poke(BigInt.from(2))),
|
||||
entry(const rust.BridgeMessageTarget.server()),
|
||||
],
|
||||
snapshot: snapshot(channels: const [], clients: const []),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Direct Messages'), findsOneWidget);
|
||||
expect(find.text('Server Activity'), findsOneWidget);
|
||||
expect(find.text('Pokes'), findsNothing);
|
||||
expect(find.text('No pokes'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('channel chat displays localized poke history in English', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: AppL10n.localizationsDelegates,
|
||||
supportedLocales: AppL10n.supportedLocales,
|
||||
home: ChatPage(
|
||||
messages: [
|
||||
ChatEntry(
|
||||
senderId: BigInt.from(2),
|
||||
senderName: 'EdisonJwa',
|
||||
message: '111',
|
||||
target: rust.BridgeMessageTarget.poke(BigInt.from(2)),
|
||||
timestamp: DateTime(2026, 5, 23, 5, 11, 17),
|
||||
),
|
||||
],
|
||||
snapshot: snapshot(channels: const [], clients: const []),
|
||||
initialTarget: const rust.BridgeMessageTarget.channel(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('<05:11:17> "EdisonJwa" pokes you: 111'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('channel chat displays localized poke history in Chinese', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
locale: const Locale('zh'),
|
||||
localizationsDelegates: AppL10n.localizationsDelegates,
|
||||
supportedLocales: AppL10n.supportedLocales,
|
||||
home: ChatPage(
|
||||
messages: [
|
||||
ChatEntry(
|
||||
senderId: BigInt.from(2),
|
||||
senderName: 'EdisonJwa',
|
||||
message: '',
|
||||
target: rust.BridgeMessageTarget.poke(BigInt.from(2)),
|
||||
timestamp: DateTime(2026, 5, 23, 5, 10, 37),
|
||||
),
|
||||
],
|
||||
snapshot: snapshot(channels: const [], clients: const []),
|
||||
initialTarget: const rust.BridgeMessageTarget.channel(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('<05:10:37> “EdisonJwa”戳了你一下'), findsOneWidget);
|
||||
});
|
||||
|
||||
test('blocks channel chat when no voice channel is joined', () {
|
||||
expect(
|
||||
canSendToChatTarget(const rust.BridgeMessageTarget.channel(), null),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
chatSendBlockedReason(const rust.BridgeMessageTarget.channel(), null),
|
||||
'Join a channel to send messages.',
|
||||
);
|
||||
expect(
|
||||
canSendToChatTarget(
|
||||
const rust.BridgeMessageTarget.channel(),
|
||||
BigInt.from(10),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
chatSendBlockedReason(
|
||||
const rust.BridgeMessageTarget.channel(),
|
||||
BigInt.from(10),
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
canSendToChatTarget(
|
||||
rust.BridgeMessageTarget.client(BigInt.from(2)),
|
||||
null,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('chat message TeamSpeak links use internal handler', (
|
||||
tester,
|
||||
) async {
|
||||
Ts3ServerLink? tapped;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ChatPage(
|
||||
messages: [
|
||||
ChatEntry(
|
||||
senderId: BigInt.one,
|
||||
senderName: 'Sender',
|
||||
message:
|
||||
'ts3server://teamspeak.app%3Faddbookmark%3DVigorous%20Pro/',
|
||||
target: rust.BridgeMessageTarget.server(),
|
||||
),
|
||||
],
|
||||
snapshot: snapshot(channels: const [], clients: const []),
|
||||
initialTarget: const rust.BridgeMessageTarget.server(),
|
||||
onTs3ServerLink: (link) async => tapped = link,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(
|
||||
find.text('ts3server://teamspeak.app%3Faddbookmark%3DVigorous%20Pro/'),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(tapped, isNotNull);
|
||||
expect(tapped!.host, 'teamspeak.app');
|
||||
expect(tapped!.addBookmark, 'Vigorous Pro');
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user