every 200ms flush shifted all sliver indices (reverse list), and the per-row ObjectKeys without findChildIndexCallback made the framework deactivate and re-inflate every built row instead of moving it. add the callback so elements move, and cache ChatMessage widget instances per message (Expando) so unchanged rows skip build() entirely via the identical-widget short-circuit. IRCMessage.renderRevision invalidates rows the CLEARCHAT/CLEARMSG handlers mutate in place. also decode emotes/badges at display size (memCacheHeight = logical size x devicePixelRatio) instead of the CDN's 3x-4x intrinsic size: several-fold less image memory and raster cost, animation preserved (ResizeImage applies the target size to the whole codec). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
726 lines
22 KiB
Dart
726 lines
22 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:frosty/constants.dart';
|
|
import 'package:frosty/models/irc.dart';
|
|
|
|
import '../fixtures/irc_messages.dart';
|
|
|
|
void main() {
|
|
group('IRCMessage.fromString', () {
|
|
group('basic parsing', () {
|
|
test('parses basic chat message correctly', () {
|
|
final msg = IRCMessage.fromString(basicPrivmsg);
|
|
|
|
expect(msg.user, 'testuser');
|
|
expect(msg.message, 'Hello World');
|
|
expect(msg.command, Command.privateMessage);
|
|
expect(msg.tags['display-name'], 'TestUser');
|
|
expect(msg.tags['color'], '#FF0000');
|
|
expect(msg.tags['user-id'], '67890');
|
|
expect(msg.tags['room-id'], '12345');
|
|
});
|
|
|
|
test('parses message with escaped spaces in tags', () {
|
|
final msg = IRCMessage.fromString(messageWithEscapedSpaces);
|
|
|
|
// \\s should be converted to actual space
|
|
expect(msg.tags['badge-info'], 'subscriber/12 months');
|
|
expect(msg.message, 'test message');
|
|
});
|
|
|
|
test('unescapes the full IRCv3 tag escape set', () {
|
|
final msg = IRCMessage.fromString(messageWithEscapedTagChars);
|
|
|
|
expect(msg.tags['a-space'], 'one two');
|
|
expect(msg.tags['a-semi'], 'left;right');
|
|
expect(msg.tags['a-slash'], r'a\b');
|
|
// Only the first '=' splits key/value; later '=' stays in the value.
|
|
expect(msg.tags['a-equals'], 'k=v');
|
|
// `\\` consumes both backslashes; the following 's' is a literal letter,
|
|
// not a `\s` space escape (single-pass unescaping).
|
|
expect(msg.tags['a-double'], r'x\sy');
|
|
});
|
|
|
|
test('falls back to login tag when prefix has no "!"', () {
|
|
// An unexpected prefix shape must not throw and drop the message.
|
|
final msg = IRCMessage.fromString(messageNoBangPrefix);
|
|
|
|
expect(msg.user, 'fallbackuser');
|
|
expect(msg.message, 'server style');
|
|
});
|
|
|
|
test('handles message with no color tag', () {
|
|
final msg = IRCMessage.fromString(messageNoColor);
|
|
|
|
// Tags with empty values are skipped during parsing
|
|
expect(msg.tags['color'], isNull);
|
|
expect(msg.user, 'nocolor');
|
|
});
|
|
|
|
test('splits message into words correctly', () {
|
|
final msg = IRCMessage.fromString(basicPrivmsg);
|
|
|
|
expect(msg.split, ['Hello', 'World']);
|
|
});
|
|
});
|
|
|
|
group('command detection', () {
|
|
test('identifies PRIVMSG command', () {
|
|
final msg = IRCMessage.fromString(basicPrivmsg);
|
|
expect(msg.command, Command.privateMessage);
|
|
});
|
|
|
|
test('identifies CLEARCHAT command for user ban', () {
|
|
final msg = IRCMessage.fromString(clearChatUser);
|
|
expect(msg.command, Command.clearChat);
|
|
expect(msg.message, 'banneduser');
|
|
expect(msg.tags['ban-duration'], '600');
|
|
});
|
|
|
|
test('identifies CLEARCHAT command for full chat clear', () {
|
|
final msg = IRCMessage.fromString(clearChatAll);
|
|
expect(msg.command, Command.clearChat);
|
|
expect(msg.message, isNull);
|
|
});
|
|
|
|
test('identifies CLEARMSG command', () {
|
|
final msg = IRCMessage.fromString(clearMsg);
|
|
expect(msg.command, Command.clearMessage);
|
|
expect(msg.tags['target-msg-id'], 'target-msg-abc123');
|
|
});
|
|
|
|
test('identifies NOTICE command', () {
|
|
final msg = IRCMessage.fromString(noticeMessage);
|
|
expect(msg.command, Command.notice);
|
|
expect(msg.tags['msg-id'], 'slow_on');
|
|
});
|
|
|
|
test('identifies USERNOTICE command', () {
|
|
final msg = IRCMessage.fromString(userNoticeSubscription);
|
|
expect(msg.command, Command.userNotice);
|
|
expect(msg.tags['msg-id'], 'sub');
|
|
});
|
|
|
|
test('identifies ROOMSTATE command', () {
|
|
final msg = IRCMessage.fromString(roomState);
|
|
expect(msg.command, Command.roomState);
|
|
expect(msg.tags['slow'], '0');
|
|
expect(msg.tags['emote-only'], '0');
|
|
});
|
|
|
|
test('identifies USERSTATE command', () {
|
|
final msg = IRCMessage.fromString(userState);
|
|
expect(msg.command, Command.userState);
|
|
expect(msg.tags['mod'], '0');
|
|
});
|
|
|
|
test('identifies GLOBALUSERSTATE command', () {
|
|
final msg = IRCMessage.fromString(globalUserState);
|
|
expect(msg.command, Command.globalUserState);
|
|
});
|
|
});
|
|
|
|
group('emote parsing', () {
|
|
test('extracts local emotes from emote tag', () {
|
|
final msg = IRCMessage.fromString(privmsgWithEmotes);
|
|
|
|
expect(msg.localEmotes, isNotNull);
|
|
expect(msg.localEmotes!.containsKey('Kappa'), isTrue);
|
|
expect(msg.localEmotes!.containsKey('LUL'), isTrue);
|
|
|
|
// Verify emote URLs are constructed correctly
|
|
expect(
|
|
msg.localEmotes!['Kappa']!.url,
|
|
contains('static-cdn.jtvnw.net/emoticons/v2/25'),
|
|
);
|
|
});
|
|
|
|
test('handles multiple instances of same emote', () {
|
|
final msg = IRCMessage.fromString(privmsgWithMultipleEmotes);
|
|
|
|
// Should still only have one entry in localEmotes map
|
|
expect(msg.localEmotes!.length, 1);
|
|
expect(msg.localEmotes!['Kappa'], isNotNull);
|
|
});
|
|
|
|
test('handles message with no emotes', () {
|
|
final msg = IRCMessage.fromString(basicPrivmsg);
|
|
|
|
expect(msg.localEmotes, isEmpty);
|
|
});
|
|
});
|
|
|
|
group('IRC action (/me) detection', () {
|
|
test('detects /me action messages', () {
|
|
final msg = IRCMessage.fromString(actionMessage);
|
|
|
|
expect(msg.action, isTrue);
|
|
expect(msg.message, 'is testing');
|
|
// Action prefix and suffix should be stripped
|
|
expect(msg.message, isNot(contains('\x01')));
|
|
});
|
|
|
|
test('non-action messages have action=false', () {
|
|
final msg = IRCMessage.fromString(basicPrivmsg);
|
|
expect(msg.action, isFalse);
|
|
});
|
|
});
|
|
|
|
group('mention detection', () {
|
|
test('detects when message mentions logged-in user', () {
|
|
final msg = IRCMessage.fromString(
|
|
mentionMessage,
|
|
userLogin: 'targetuser',
|
|
);
|
|
|
|
expect(msg.mention, isTrue);
|
|
});
|
|
|
|
test('mention detection lowercases the message', () {
|
|
// Note: The code lowercases the message but compares against userLogin as-is
|
|
// So userLogin should be provided in lowercase for proper matching
|
|
final msg = IRCMessage.fromString(
|
|
mentionMessage,
|
|
userLogin: 'targetuser', // lowercase to match the lowercased message
|
|
);
|
|
|
|
expect(msg.mention, isTrue);
|
|
});
|
|
|
|
test('no mention when user is not mentioned', () {
|
|
final msg = IRCMessage.fromString(
|
|
basicPrivmsg,
|
|
userLogin: 'someotheruser',
|
|
);
|
|
|
|
expect(msg.mention, isFalse);
|
|
});
|
|
|
|
test('mention is false when userLogin not provided', () {
|
|
final msg = IRCMessage.fromString(basicPrivmsg);
|
|
expect(msg.mention, isFalse);
|
|
});
|
|
});
|
|
|
|
group('Unicode handling', () {
|
|
test('removes invalid Unicode characters', () {
|
|
final msg = IRCMessage.fromString(messageWithInvalidUnicode);
|
|
|
|
// The invalid Unicode character should be stripped
|
|
expect(msg.message, 'HelloWorld');
|
|
expect(msg.message, isNot(contains('\u{E0000}')));
|
|
});
|
|
|
|
test('handles emoji characters correctly', () {
|
|
final msg = IRCMessage.fromString(messageWithEmoji);
|
|
|
|
expect(msg.message, contains('😀'));
|
|
expect(msg.message, contains('🎉'));
|
|
// Emojis should be separated into their own entries in split
|
|
expect(msg.split!.any((s) => s.contains('😀')), isTrue);
|
|
});
|
|
});
|
|
|
|
group('reply message handling', () {
|
|
test('parses reply message tags', () {
|
|
final msg = IRCMessage.fromString(replyMessage);
|
|
|
|
expect(msg.tags['reply-parent-display-name'], 'OriginalUser');
|
|
expect(msg.tags['reply-parent-msg-id'], 'original-123');
|
|
expect(msg.tags['reply-parent-user-login'], 'originaluser');
|
|
});
|
|
});
|
|
|
|
group('special message types', () {
|
|
test('parses moderator message', () {
|
|
final msg = IRCMessage.fromString(moderatorMessage);
|
|
|
|
expect(msg.tags['mod'], '1');
|
|
expect(msg.tags['badges'], contains('moderator'));
|
|
});
|
|
|
|
test('parses historical message', () {
|
|
final msg = IRCMessage.fromString(historicalMessage);
|
|
|
|
expect(msg.tags['historical'], '1');
|
|
});
|
|
|
|
test('parses historical message with bare (valueless) tags', () {
|
|
// The recent-messages API serializes empty tags as bare keys (e.g.
|
|
// `emotes`, `flags`) instead of `key=`. This previously threw a
|
|
// RangeError, silently dropping all recent messages.
|
|
final msg = IRCMessage.fromString(historicalMessageBareTags);
|
|
|
|
expect(msg.tags['historical'], '1');
|
|
expect(msg.tags['display-name'], 'BareUser');
|
|
expect(msg.message, 'Bare tag message');
|
|
// Bare tags should be absent from the map, not crash parsing.
|
|
expect(msg.tags.containsKey('emotes'), isFalse);
|
|
expect(msg.tags.containsKey('flags'), isFalse);
|
|
});
|
|
|
|
test('parses shared chat message with source room', () {
|
|
final msg = IRCMessage.fromString(sharedChatMessage);
|
|
|
|
expect(msg.tags['source-room-id'], '99999');
|
|
expect(msg.tags['room-id'], '12345');
|
|
});
|
|
|
|
test('parses message with URL in split', () {
|
|
final msg = IRCMessage.fromString(messageWithUrl);
|
|
|
|
expect(msg.message, contains('https://twitch.tv/channel'));
|
|
expect(msg.split, isNotNull);
|
|
expect(
|
|
msg.split!.any((word) => regexLink.hasMatch(word)),
|
|
isTrue,
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
group('IRCMessage.clearChat', () {
|
|
late List<IRCMessage> messages;
|
|
late List<IRCMessage> bufferedMessages;
|
|
|
|
setUp(() {
|
|
// Create test messages from different users
|
|
messages = [
|
|
IRCMessage.fromString(basicPrivmsg), // from testuser
|
|
IRCMessage.fromString(moderatorMessage), // from moduser
|
|
IRCMessage.fromString(
|
|
basicPrivmsg.replaceAll('testuser', 'user2').replaceAll('TestUser', 'User2'),
|
|
), // from user2
|
|
];
|
|
bufferedMessages = [];
|
|
});
|
|
|
|
test('marks all messages from banned user', () {
|
|
// Create a CLEARCHAT message targeting testuser
|
|
final clearChatMsg = IRCMessage(
|
|
raw: '',
|
|
command: Command.clearChat,
|
|
tags: {'ban-duration': '600'},
|
|
message: 'testuser',
|
|
);
|
|
|
|
IRCMessage.clearChat(
|
|
messages: messages,
|
|
bufferedMessages: bufferedMessages,
|
|
ircMessage: clearChatMsg,
|
|
);
|
|
|
|
// testuser's message should be marked as clearChat
|
|
expect(
|
|
messages.where((m) => m.user == 'testuser').first.command,
|
|
Command.clearChat,
|
|
);
|
|
// Other users' messages should be unchanged
|
|
expect(
|
|
messages.where((m) => m.user == 'moduser').first.command,
|
|
Command.privateMessage,
|
|
);
|
|
});
|
|
|
|
test('adds ban-duration tag to affected messages', () {
|
|
final clearChatMsg = IRCMessage(
|
|
raw: '',
|
|
command: Command.clearChat,
|
|
tags: {'ban-duration': '300'},
|
|
message: 'testuser',
|
|
);
|
|
|
|
IRCMessage.clearChat(
|
|
messages: messages,
|
|
bufferedMessages: bufferedMessages,
|
|
ircMessage: clearChatMsg,
|
|
);
|
|
|
|
final affectedMessage = messages.where((m) => m.user == 'testuser').first;
|
|
expect(affectedMessage.tags['ban-duration'], '300');
|
|
});
|
|
|
|
test('bumps renderRevision only on affected messages', () {
|
|
final clearChatMsg = IRCMessage(
|
|
raw: '',
|
|
command: Command.clearChat,
|
|
tags: {'ban-duration': '600'},
|
|
message: 'testuser',
|
|
);
|
|
bufferedMessages = [
|
|
IRCMessage.fromString(basicPrivmsg), // from testuser
|
|
];
|
|
|
|
IRCMessage.clearChat(
|
|
messages: messages,
|
|
bufferedMessages: bufferedMessages,
|
|
ircMessage: clearChatMsg,
|
|
);
|
|
|
|
// Mutated-in-place messages must signal render caches to invalidate.
|
|
expect(
|
|
messages.where((m) => m.user == 'testuser').first.renderRevision,
|
|
1,
|
|
);
|
|
expect(bufferedMessages.first.renderRevision, 1);
|
|
expect(
|
|
messages.where((m) => m.user == 'moduser').first.renderRevision,
|
|
0,
|
|
);
|
|
});
|
|
|
|
test('clears entire chat when no target user', () {
|
|
final clearAllMsg = IRCMessage(
|
|
raw: '',
|
|
command: Command.clearChat,
|
|
tags: {},
|
|
);
|
|
|
|
IRCMessage.clearChat(
|
|
messages: messages,
|
|
bufferedMessages: bufferedMessages,
|
|
ircMessage: clearAllMsg,
|
|
);
|
|
|
|
// Messages list should be cleared and contain only the notice
|
|
expect(messages.length, 1);
|
|
expect(messages.first.command, Command.notice);
|
|
expect(messages.first.message, 'Chat was cleared by a moderator');
|
|
});
|
|
});
|
|
|
|
group('IRCMessage.clearMessage', () {
|
|
late List<IRCMessage> messages;
|
|
late List<IRCMessage> bufferedMessages;
|
|
|
|
setUp(() {
|
|
messages = [
|
|
IRCMessage(
|
|
raw: '',
|
|
command: Command.privateMessage,
|
|
tags: {'id': 'msg-1'},
|
|
user: 'user1',
|
|
message: 'Message 1',
|
|
),
|
|
IRCMessage(
|
|
raw: '',
|
|
command: Command.privateMessage,
|
|
tags: {'id': 'msg-2'},
|
|
user: 'user2',
|
|
message: 'Message 2',
|
|
),
|
|
IRCMessage(
|
|
raw: '',
|
|
command: Command.privateMessage,
|
|
tags: {'id': 'msg-3'},
|
|
user: 'user1',
|
|
message: 'Message 3',
|
|
),
|
|
];
|
|
bufferedMessages = [];
|
|
});
|
|
|
|
test('marks specific message by ID', () {
|
|
final clearMsg = IRCMessage(
|
|
raw: '',
|
|
command: Command.clearMessage,
|
|
tags: {'target-msg-id': 'msg-2'},
|
|
);
|
|
|
|
IRCMessage.clearMessage(
|
|
messages: messages,
|
|
bufferedMessages: bufferedMessages,
|
|
ircMessage: clearMsg,
|
|
);
|
|
|
|
// Only msg-2 should be marked
|
|
expect(
|
|
messages.firstWhere((m) => m.tags['id'] == 'msg-2').command,
|
|
Command.clearMessage,
|
|
);
|
|
// Mutated-in-place message must signal render caches to invalidate.
|
|
expect(
|
|
messages.firstWhere((m) => m.tags['id'] == 'msg-2').renderRevision,
|
|
1,
|
|
);
|
|
expect(
|
|
messages.firstWhere((m) => m.tags['id'] == 'msg-1').renderRevision,
|
|
0,
|
|
);
|
|
// Other messages unchanged
|
|
expect(
|
|
messages.firstWhere((m) => m.tags['id'] == 'msg-1').command,
|
|
Command.privateMessage,
|
|
);
|
|
expect(
|
|
messages.firstWhere((m) => m.tags['id'] == 'msg-3').command,
|
|
Command.privateMessage,
|
|
);
|
|
});
|
|
|
|
test('handles non-existent message ID gracefully', () {
|
|
final clearMsg = IRCMessage(
|
|
raw: '',
|
|
command: Command.clearMessage,
|
|
tags: {'target-msg-id': 'non-existent'},
|
|
);
|
|
|
|
// Should not throw
|
|
expect(
|
|
() => IRCMessage.clearMessage(
|
|
messages: messages,
|
|
bufferedMessages: bufferedMessages,
|
|
ircMessage: clearMsg,
|
|
),
|
|
returnsNormally,
|
|
);
|
|
|
|
// All messages should be unchanged
|
|
expect(
|
|
messages.every((m) => m.command == Command.privateMessage),
|
|
isTrue,
|
|
);
|
|
});
|
|
});
|
|
|
|
group('IRCMessage.createNotice', () {
|
|
test('creates notice message with correct properties', () {
|
|
final notice = IRCMessage.createNotice(message: 'Test notice');
|
|
|
|
expect(notice.command, Command.notice);
|
|
expect(notice.message, 'Test notice');
|
|
expect(notice.raw, isEmpty);
|
|
expect(notice.tags, contains('tmi-sent-ts'));
|
|
expect(notice.tags.length, 1);
|
|
});
|
|
|
|
test('creates notice with action callback', () {
|
|
var called = false;
|
|
final notice = IRCMessage.createNotice(
|
|
message: 'Test',
|
|
actionCallback: () => called = true,
|
|
actionLabel: 'Retry',
|
|
);
|
|
|
|
expect(notice.actionCallback, isNotNull);
|
|
expect(notice.actionLabel, 'Retry');
|
|
notice.actionCallback!();
|
|
expect(called, isTrue);
|
|
});
|
|
});
|
|
|
|
group('ROOMSTATE', () {
|
|
test('creates default ROOMSTATE', () {
|
|
const roomstate = ROOMSTATE();
|
|
|
|
expect(roomstate.emoteOnly, '0');
|
|
expect(roomstate.followersOnly, '-1');
|
|
expect(roomstate.r9k, '0');
|
|
expect(roomstate.slowMode, '0');
|
|
expect(roomstate.subMode, '0');
|
|
});
|
|
|
|
test('updates from IRC message', () {
|
|
const original = ROOMSTATE();
|
|
final ircMsg = IRCMessage(
|
|
raw: '',
|
|
command: Command.roomState,
|
|
tags: {
|
|
'emote-only': '1',
|
|
'followers-only': '10',
|
|
'slow': '30',
|
|
},
|
|
);
|
|
|
|
final updated = original.fromIRCMessage(ircMsg);
|
|
|
|
expect(updated.emoteOnly, '1');
|
|
expect(updated.followersOnly, '10');
|
|
expect(updated.slowMode, '30');
|
|
// Unchanged values should keep original
|
|
expect(updated.r9k, '0');
|
|
expect(updated.subMode, '0');
|
|
});
|
|
});
|
|
|
|
group('IRCMessage.clearChat — buffered messages', () {
|
|
test('marks buffered messages from banned user', () {
|
|
final messages = <IRCMessage>[];
|
|
final bufferedMessages = [
|
|
IRCMessage(
|
|
raw: '',
|
|
command: Command.privateMessage,
|
|
tags: {'id': 'buf-1'},
|
|
user: 'banneduser',
|
|
message: 'Buffered msg',
|
|
),
|
|
IRCMessage(
|
|
raw: '',
|
|
command: Command.privateMessage,
|
|
tags: {'id': 'buf-2'},
|
|
user: 'innocentuser',
|
|
message: 'Other msg',
|
|
),
|
|
];
|
|
|
|
final clearChatMsg = IRCMessage(
|
|
raw: '',
|
|
command: Command.clearChat,
|
|
tags: {'ban-duration': '600'},
|
|
message: 'banneduser',
|
|
);
|
|
|
|
IRCMessage.clearChat(
|
|
messages: messages,
|
|
bufferedMessages: bufferedMessages,
|
|
ircMessage: clearChatMsg,
|
|
);
|
|
|
|
expect(bufferedMessages[0].command, Command.clearChat);
|
|
expect(bufferedMessages[0].tags['ban-duration'], '600');
|
|
expect(bufferedMessages[1].command, Command.privateMessage);
|
|
});
|
|
|
|
test('full chat clear also clears buffered messages', () {
|
|
final messages = [
|
|
IRCMessage(
|
|
raw: '',
|
|
command: Command.privateMessage,
|
|
tags: {},
|
|
message: 'msg',
|
|
),
|
|
];
|
|
final bufferedMessages = [
|
|
IRCMessage(
|
|
raw: '',
|
|
command: Command.privateMessage,
|
|
tags: {},
|
|
message: 'buffered',
|
|
),
|
|
];
|
|
|
|
final clearAllMsg = IRCMessage(
|
|
raw: '',
|
|
command: Command.clearChat,
|
|
tags: {},
|
|
);
|
|
|
|
IRCMessage.clearChat(
|
|
messages: messages,
|
|
bufferedMessages: bufferedMessages,
|
|
ircMessage: clearAllMsg,
|
|
);
|
|
|
|
expect(bufferedMessages, isEmpty);
|
|
});
|
|
});
|
|
|
|
group('IRCMessage.clearMessage — buffered messages', () {
|
|
test('marks buffered message by ID', () {
|
|
final messages = <IRCMessage>[];
|
|
final bufferedMessages = [
|
|
IRCMessage(
|
|
raw: '',
|
|
command: Command.privateMessage,
|
|
tags: {'id': 'buf-target'},
|
|
user: 'someuser',
|
|
message: 'To be deleted',
|
|
),
|
|
];
|
|
|
|
final clearMsg = IRCMessage(
|
|
raw: '',
|
|
command: Command.clearMessage,
|
|
tags: {'target-msg-id': 'buf-target'},
|
|
);
|
|
|
|
IRCMessage.clearMessage(
|
|
messages: messages,
|
|
bufferedMessages: bufferedMessages,
|
|
ircMessage: clearMsg,
|
|
);
|
|
|
|
expect(bufferedMessages.first.command, Command.clearMessage);
|
|
});
|
|
});
|
|
|
|
group('IRCMessage edge cases', () {
|
|
test('parses message with empty body', () {
|
|
final msg = IRCMessage.fromString(messageEmptyBody);
|
|
|
|
expect(msg.command, Command.privateMessage);
|
|
// Empty trailing content after the colon
|
|
expect(msg.message, isEmpty);
|
|
});
|
|
|
|
test('parses emote-only message', () {
|
|
final msg = IRCMessage.fromString(messageOnlyEmotes);
|
|
|
|
expect(msg.command, Command.privateMessage);
|
|
expect(msg.message, 'Kappa Kappa');
|
|
expect(msg.localEmotes, isNotNull);
|
|
expect(msg.localEmotes!.containsKey('Kappa'), isTrue);
|
|
});
|
|
|
|
test('returns Command.none for a truncated line without a command', () {
|
|
// A malformed/truncated line (e.g. from the recent-messages history API)
|
|
// whose message section is a single token used to throw a RangeError on
|
|
// splitMessage[1]. It must now parse safely as a none command.
|
|
final msg = IRCMessage.fromString('@historical=1 :justaprefix');
|
|
|
|
expect(msg.command, Command.none);
|
|
expect(msg.tags['historical'], '1');
|
|
});
|
|
|
|
test('returns Command.none for a line with no separating space', () {
|
|
// No space means there are no tags to slice and no body to parse; must
|
|
// not throw on the substring/split calls.
|
|
final msg = IRCMessage.fromString('@onlytags');
|
|
|
|
expect(msg.command, Command.none);
|
|
});
|
|
});
|
|
|
|
group('USERSTATE', () {
|
|
test('creates default USERSTATE', () {
|
|
const userstate = USERSTATE();
|
|
|
|
expect(userstate.color, '');
|
|
expect(userstate.displayName, '');
|
|
expect(userstate.mod, isFalse);
|
|
expect(userstate.subscriber, isFalse);
|
|
});
|
|
|
|
test('updates from IRC message', () {
|
|
const original = USERSTATE();
|
|
final ircMsg = IRCMessage(
|
|
raw: '@color=#FF0000;display-name=TestUser;mod=1;subscriber=1',
|
|
command: Command.userState,
|
|
tags: {
|
|
'color': '#FF0000',
|
|
'display-name': 'TestUser',
|
|
'mod': '1',
|
|
'subscriber': '1',
|
|
},
|
|
);
|
|
|
|
final updated = original.fromIRCMessage(ircMsg);
|
|
|
|
expect(updated.color, '#FF0000');
|
|
expect(updated.displayName, 'TestUser');
|
|
expect(updated.mod, isTrue);
|
|
expect(updated.subscriber, isTrue);
|
|
});
|
|
|
|
test('mod=0 is false', () {
|
|
const original = USERSTATE();
|
|
final ircMsg = IRCMessage(
|
|
raw: '',
|
|
command: Command.userState,
|
|
tags: {'mod': '0'},
|
|
);
|
|
|
|
final updated = original.fromIRCMessage(ircMsg);
|
|
expect(updated.mod, isFalse);
|
|
});
|
|
});
|
|
}
|