RawKeyEventData classes support diagnostic and equality (#86679)

This PR changes RawKeyEventData so that they use Diagnosticable to construct toString, and use operator== and hashCode to compare.
diff --git a/packages/flutter/lib/src/services/raw_keyboard.dart b/packages/flutter/lib/src/services/raw_keyboard.dart
index 512ed64..ba67416 100644
--- a/packages/flutter/lib/src/services/raw_keyboard.dart
+++ b/packages/flutter/lib/src/services/raw_keyboard.dart
@@ -115,7 +115,7 @@
 ///    reference to [RawKeyEventData] subclasses.
 ///  * [RawKeyboard], which uses these interfaces to expose key data.
 @immutable
-abstract class RawKeyEventData {
+abstract class RawKeyEventData with Diagnosticable {
   /// Abstract const constructor. This constructor enables subclasses to provide
   /// const constructors so that they can be used in const expressions.
   const RawKeyEventData();
diff --git a/packages/flutter/lib/src/services/raw_keyboard_android.dart b/packages/flutter/lib/src/services/raw_keyboard_android.dart
index 8a68645..ddcd604 100644
--- a/packages/flutter/lib/src/services/raw_keyboard_android.dart
+++ b/packages/flutter/lib/src/services/raw_keyboard_android.dart
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:ui' show hashValues;
 
 import 'package:flutter/foundation.dart';
 
@@ -286,6 +287,42 @@
     }
   }
 
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(DiagnosticsProperty<int>('flags', flags));
+    properties.add(DiagnosticsProperty<int>('codePoint', codePoint));
+    properties.add(DiagnosticsProperty<int>('plainCodePoint', plainCodePoint));
+    properties.add(DiagnosticsProperty<int>('keyCode', keyCode));
+    properties.add(DiagnosticsProperty<int>('scanCode', scanCode));
+    properties.add(DiagnosticsProperty<int>('metaState', metaState));
+  }
+
+  @override
+  bool operator==(Object other) {
+    if (identical(this, other))
+      return true;
+    if (other.runtimeType != runtimeType)
+      return false;
+    return other is RawKeyEventDataAndroid
+        && other.flags == flags
+        && other.codePoint == codePoint
+        && other.plainCodePoint == plainCodePoint
+        && other.keyCode == keyCode
+        && other.scanCode == scanCode
+        && other.metaState == metaState;
+  }
+
+  @override
+  int get hashCode => hashValues(
+    flags,
+    codePoint,
+    plainCodePoint,
+    keyCode,
+    scanCode,
+    metaState,
+  );
+
   // Modifier key masks.
 
   /// No modifier keys are pressed in the [metaState] field.
@@ -430,11 +467,4 @@
   /// it's much easier to use [isModifierPressed] if you just want to know if
   /// a modifier is pressed.
   static const int modifierScrollLock = 0x400000;
-
-  @override
-  String toString() {
-    return '${objectRuntimeType(this, 'RawKeyEventDataAndroid')}(keyLabel: $keyLabel flags: $flags, codePoint: $codePoint, '
-      'keyCode: $keyCode, scanCode: $scanCode, metaState: $metaState, '
-      'modifiers down: $modifiersPressed)';
-  }
 }
diff --git a/packages/flutter/lib/src/services/raw_keyboard_fuchsia.dart b/packages/flutter/lib/src/services/raw_keyboard_fuchsia.dart
index 55059cc..ad6e6a8 100644
--- a/packages/flutter/lib/src/services/raw_keyboard_fuchsia.dart
+++ b/packages/flutter/lib/src/services/raw_keyboard_fuchsia.dart
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:ui' show hashValues;
 
 import 'package:flutter/foundation.dart';
 
@@ -159,6 +160,33 @@
     }
   }
 
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(DiagnosticsProperty<int>('hidUsage', hidUsage));
+    properties.add(DiagnosticsProperty<int>('codePoint', codePoint));
+    properties.add(DiagnosticsProperty<int>('modifiers', modifiers));
+  }
+
+  @override
+  bool operator==(Object other) {
+    if (identical(this, other))
+      return true;
+    if (other.runtimeType != runtimeType)
+      return false;
+    return other is RawKeyEventDataFuchsia
+        && other.hidUsage == hidUsage
+        && other.codePoint == codePoint
+        && other.modifiers == modifiers;
+  }
+
+  @override
+  int get hashCode => hashValues(
+    hidUsage,
+    codePoint,
+    modifiers,
+  );
+
   // Keyboard modifier masks for Fuchsia modifiers.
 
   /// The [modifiers] field indicates that no modifier keys are pressed if it
@@ -272,10 +300,4 @@
   /// it's much easier to use [isModifierPressed] if you just want to know if
   /// a modifier is pressed.
   static const int modifierMeta = modifierLeftMeta | modifierRightMeta;
-
-  @override
-  String toString() {
-    return '${objectRuntimeType(this, 'RawKeyEventDataFuchsia')}(hidUsage: $hidUsage, codePoint: $codePoint, modifiers: $modifiers, '
-        'modifiers down: $modifiersPressed)';
-  }
 }
diff --git a/packages/flutter/lib/src/services/raw_keyboard_ios.dart b/packages/flutter/lib/src/services/raw_keyboard_ios.dart
index 8d5735c..f356843 100644
--- a/packages/flutter/lib/src/services/raw_keyboard_ios.dart
+++ b/packages/flutter/lib/src/services/raw_keyboard_ios.dart
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:ui' show hashValues;
+
 import 'package:flutter/foundation.dart';
 
 import 'keyboard_key.dart';
@@ -253,6 +255,36 @@
     }
   }
 
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+        properties.add(DiagnosticsProperty<String>('characters', characters));
+        properties.add(DiagnosticsProperty<String>('charactersIgnoringModifiers', charactersIgnoringModifiers));
+        properties.add(DiagnosticsProperty<int>('keyCode', keyCode));
+        properties.add(DiagnosticsProperty<int>('modifiers', modifiers));
+  }
+
+  @override
+  bool operator==(Object other) {
+    if (identical(this, other))
+      return true;
+    if (other.runtimeType != runtimeType)
+      return false;
+    return other is RawKeyEventDataIos
+        && other.characters == characters
+        && other.charactersIgnoringModifiers == charactersIgnoringModifiers
+        && other.keyCode == keyCode
+        && other.modifiers == modifiers;
+  }
+
+  @override
+  int get hashCode => hashValues(
+    characters,
+    charactersIgnoringModifiers,
+    keyCode,
+    modifiers,
+  );
+
   // Modifier key masks. See Apple's UIKey documentation
   // https://developer.apple.com/documentation/uikit/uikeymodifierflags?language=objc
   // https://opensource.apple.com/source/IOHIDFamily/IOHIDFamily-86/IOHIDSystem/IOKit/hidsystem/IOLLEvent.h.auto.html
@@ -361,11 +393,4 @@
   /// applications to mask off the device-dependent modifier flags, including
   /// event coalescing information.
   static const int deviceIndependentMask = 0xffff0000;
-
-  @override
-  String toString() {
-    return '${objectRuntimeType(this, 'RawKeyEventDataIos')}(keyLabel: $keyLabel, keyCode: $keyCode, characters: $characters,'
-        ' unmodifiedCharacters: $charactersIgnoringModifiers, modifiers: $modifiers, '
-        'modifiers down: $modifiersPressed)';
-  }
 }
diff --git a/packages/flutter/lib/src/services/raw_keyboard_linux.dart b/packages/flutter/lib/src/services/raw_keyboard_linux.dart
index f0f2510..e0af412 100644
--- a/packages/flutter/lib/src/services/raw_keyboard_linux.dart
+++ b/packages/flutter/lib/src/services/raw_keyboard_linux.dart
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:ui' show hashValues;
 
 import 'package:flutter/foundation.dart';
 
@@ -117,11 +118,40 @@
   }
 
   @override
-  String toString() {
-    return '${objectRuntimeType(this, 'RawKeyEventDataLinux')}(keyLabel: $keyLabel, keyCode: $keyCode, scanCode: $scanCode,'
-        ' unicodeScalarValues: $unicodeScalarValues, modifiers: $modifiers, '
-        'modifiers down: $modifiersPressed)';
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(DiagnosticsProperty<String>('toolkit', keyHelper.debugToolkit));
+    properties.add(DiagnosticsProperty<int>('unicodeScalarValues', unicodeScalarValues));
+    properties.add(DiagnosticsProperty<int>('scanCode', scanCode));
+    properties.add(DiagnosticsProperty<int>('keyCode', keyCode));
+    properties.add(DiagnosticsProperty<int>('modifiers', modifiers));
+    properties.add(DiagnosticsProperty<bool>('isDown', isDown));
   }
+
+  @override
+  bool operator==(Object other) {
+    if (identical(this, other))
+      return true;
+    if (other.runtimeType != runtimeType)
+      return false;
+    return other is RawKeyEventDataLinux
+        && other.keyHelper.runtimeType == keyHelper.runtimeType
+        && other.unicodeScalarValues == unicodeScalarValues
+        && other.scanCode == scanCode
+        && other.keyCode == keyCode
+        && other.modifiers == modifiers
+        && other.isDown == isDown;
+  }
+
+  @override
+  int get hashCode => hashValues(
+    keyHelper.runtimeType,
+    unicodeScalarValues,
+    scanCode,
+    keyCode,
+    modifiers,
+    isDown,
+  );
 }
 
 /// Abstract class for window-specific key mappings.
@@ -141,6 +171,11 @@
     }
   }
 
+  /// Returns the name for the toolkit.
+  ///
+  /// This is used in debug mode to generate readable string.
+  String get debugToolkit;
+
   /// Returns a [KeyboardSide] enum value that describes which side or sides of
   /// the given keyboard modifier key were pressed at the time of this event.
   KeyboardSide getModifierSide(ModifierKey key);
@@ -204,6 +239,9 @@
   /// {@macro flutter.services.GLFWKeyHelper.modifierCapsLock}
   static const int modifierNumericPad = 0x0020;
 
+  @override
+  String get debugToolkit => 'GLFW';
+
   int _mergeModifiers({required int modifiers, required int keyCode, required bool isDown}) {
     // GLFW Key codes for modifier keys.
     const int shiftLeftKeyCode = 340;
@@ -343,6 +381,9 @@
   /// {@macro flutter.services.GtkKeyHelper.modifierShift}
   static const int modifierMeta = 1 << 26;
 
+  @override
+  String get debugToolkit => 'GTK';
+
   int _mergeModifiers({required int modifiers, required int keyCode, required bool isDown}) {
     // GTK Key codes for modifier keys.
     const int shiftLeftKeyCode = 0xffe1;
diff --git a/packages/flutter/lib/src/services/raw_keyboard_macos.dart b/packages/flutter/lib/src/services/raw_keyboard_macos.dart
index bbc3ee8..63eab78 100644
--- a/packages/flutter/lib/src/services/raw_keyboard_macos.dart
+++ b/packages/flutter/lib/src/services/raw_keyboard_macos.dart
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:ui' show hashValues;
 
 import 'package:flutter/foundation.dart';
 
@@ -216,6 +217,36 @@
     return logicalKey != LogicalKeyboardKey.fn;
   }
 
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(DiagnosticsProperty<String>('characters', characters));
+    properties.add(DiagnosticsProperty<String>('charactersIgnoringModifiers', charactersIgnoringModifiers));
+    properties.add(DiagnosticsProperty<int>('keyCode', keyCode));
+    properties.add(DiagnosticsProperty<int>('modifiers', modifiers));
+  }
+
+  @override
+  bool operator==(Object other) {
+    if (identical(this, other))
+      return true;
+    if (other.runtimeType != runtimeType)
+      return false;
+    return other is RawKeyEventDataMacOs
+        && other.characters == characters
+        && other.charactersIgnoringModifiers == charactersIgnoringModifiers
+        && other.keyCode == keyCode
+        && other.modifiers == modifiers;
+  }
+
+  @override
+  int get hashCode => hashValues(
+    characters,
+    charactersIgnoringModifiers,
+    keyCode,
+    modifiers,
+  );
+
   /// Returns true if the given label represents an unprintable key.
   ///
   /// Examples of unprintable keys are "NSUpArrowFunctionKey = 0xF700"
@@ -341,11 +372,4 @@
   /// applications to mask off the device-dependent modifier flags, including
   /// event coalescing information.
   static const int deviceIndependentMask = 0xffff0000;
-
-  @override
-  String toString() {
-    return '${objectRuntimeType(this, 'RawKeyEventDataMacOs')}(keyLabel: $keyLabel, keyCode: $keyCode, characters: $characters,'
-        ' unmodifiedCharacters: $charactersIgnoringModifiers, modifiers: $modifiers, '
-        'modifiers down: $modifiersPressed)';
-  }
 }
diff --git a/packages/flutter/lib/src/services/raw_keyboard_web.dart b/packages/flutter/lib/src/services/raw_keyboard_web.dart
index 232bfc8..70e2b94 100644
--- a/packages/flutter/lib/src/services/raw_keyboard_web.dart
+++ b/packages/flutter/lib/src/services/raw_keyboard_web.dart
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:ui' show hashValues;
 
 import 'package:flutter/foundation.dart';
 
@@ -29,18 +30,28 @@
 
   /// The `KeyboardEvent.code` corresponding to this event.
   ///
+  /// The [code] represents a physical key on the keyboard, a value that isn't
+  /// altered by keyboard layout or the state of the modifier keys.
+  ///
   /// See <https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code>
   /// for more information.
   final String code;
 
   /// The `KeyboardEvent.key` corresponding to this event.
   ///
+  /// The [key] represents the key pressed by the user, taking into
+  /// consideration the state of modifier keys such as Shift as well as the
+  /// keyboard locale and layout.
+  ///
   /// See <https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key>
   /// for more information.
   final String key;
 
   /// The `KeyboardEvent.location` corresponding to this event.
   ///
+  /// The [location] represents the location of the key on the keyboard or other
+  /// input device, such as left or right modifier keys, or Numpad keys.
+  ///
   /// See <https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/location>
   /// for more information.
   final int location;
@@ -126,6 +137,36 @@
     return KeyboardSide.any;
   }
 
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+        properties.add(DiagnosticsProperty<String>('code', code));
+        properties.add(DiagnosticsProperty<String>('key', key));
+        properties.add(DiagnosticsProperty<int>('location', location));
+        properties.add(DiagnosticsProperty<int>('metaState', metaState));
+  }
+
+  @override
+  bool operator==(Object other) {
+    if (identical(this, other))
+      return true;
+    if (other.runtimeType != runtimeType)
+      return false;
+    return other is RawKeyEventDataWeb
+        && other.code == code
+        && other.key == key
+        && other.location == location
+        && other.metaState == metaState;
+  }
+
+  @override
+  int get hashCode => hashValues(
+    code,
+    key,
+    location,
+    metaState,
+  );
+
   // Modifier key masks.
 
   /// No modifier keys are pressed in the [metaState] field.
@@ -190,10 +231,4 @@
   /// it's much easier to use [isModifierPressed] if you just want to know if
   /// a modifier is pressed.
   static const int modifierScrollLock = 0x40;
-
-  @override
-  String toString() {
-    return '${objectRuntimeType(this, 'RawKeyEventDataWeb')}(keyLabel: $keyLabel, code: $code, '
-        'location: $location, metaState: $metaState, modifiers down: $modifiersPressed)';
-  }
 }
diff --git a/packages/flutter/lib/src/services/raw_keyboard_windows.dart b/packages/flutter/lib/src/services/raw_keyboard_windows.dart
index 7e57090..4fb2c7f 100644
--- a/packages/flutter/lib/src/services/raw_keyboard_windows.dart
+++ b/packages/flutter/lib/src/services/raw_keyboard_windows.dart
@@ -2,6 +2,9 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:ui' show hashValues;
+
+import 'package:flutter/foundation.dart';
 
 import 'keyboard_key.dart';
 import 'keyboard_maps.dart';
@@ -197,6 +200,36 @@
     return keyCode != _vkProcessKey;
   }
 
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(DiagnosticsProperty<int>('keyCode', keyCode));
+    properties.add(DiagnosticsProperty<int>('scanCode', scanCode));
+    properties.add(DiagnosticsProperty<int>('characterCodePoint', characterCodePoint));
+    properties.add(DiagnosticsProperty<int>('modifiers', modifiers));
+  }
+
+  @override
+  bool operator==(Object other) {
+    if (identical(this, other))
+      return true;
+    if (other.runtimeType != runtimeType)
+      return false;
+    return other is RawKeyEventDataWindows
+        && other.keyCode == keyCode
+        && other.scanCode == scanCode
+        && other.characterCodePoint == characterCodePoint
+        && other.modifiers == modifiers;
+  }
+
+  @override
+  int get hashCode => hashValues(
+    keyCode,
+    scanCode,
+    characterCodePoint,
+    modifiers,
+  );
+
   // These are not the values defined by the Windows header for each modifier. Since they
   // can't be packaged into a single int, we are re-defining them here to reduce the size
   // of the message from the embedder. Embedders should map these values to the native key codes.
diff --git a/packages/flutter/test/services/raw_keyboard_test.dart b/packages/flutter/test/services/raw_keyboard_test.dart
index b11860e..5db4e25 100644
--- a/packages/flutter/test/services/raw_keyboard_test.dart
+++ b/packages/flutter/test/services/raw_keyboard_test.dart
@@ -1045,6 +1045,59 @@
       expect(message, equals(<String, dynamic>{ 'handled': true }));
       tester.binding.defaultBinaryMessenger.setMockMessageHandler(SystemChannels.keyEvent.name, null);
     });
+
+    test('data.toString', () {
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'android',
+        'keyCode': 29,
+        'plainCodePoint': 97,
+        'codePoint': 65,
+        'character': 'A',
+        'scanCode': 30,
+        'metaState': 0x0,
+        'source': 0x101, // Keyboard source.
+        'repeatCount': 42,
+      }).data.toString(), equalsIgnoringHashCodes(
+        'RawKeyEventDataAndroid#00000('
+        'flags: 0, codePoint: 65, plainCodePoint: 97, keyCode: 29, '
+        'scanCode: 30, metaState: 0)'));
+    });
+
+    test('data.equality', () {
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'android',
+        'keyCode': 29,
+        'plainCodePoint': 97,
+        'codePoint': 65,
+        'character': 'A',
+        'scanCode': 30,
+        'metaState': 0x0,
+        'source': 0x101, // Keyboard source.
+        'repeatCount': 42,
+      }).data, const RawKeyEventDataAndroid(
+        flags: 0,
+        codePoint: 65,
+        plainCodePoint: 97,
+        keyCode: 29,
+        scanCode: 30,
+        metaState: 0,
+      ));
+
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'android',
+        'keyCode': 29,
+        'plainCodePoint': 97,
+        'codePoint': 65,
+        'character': 'A',
+        'scanCode': 30,
+        'metaState': 0x0,
+        'source': 0x101, // Keyboard source.
+        'repeatCount': 42,
+      }).data, isNot(equals(const RawKeyEventDataAndroid())));
+    });
   }, skip: isBrowser); // This is an Android-specific group.
 
   group('RawKeyEventDataFuchsia', () {
@@ -1163,6 +1216,42 @@
       expect(data.logicalKey, equals(LogicalKeyboardKey.shiftLeft));
       expect(data.keyLabel, isEmpty);
     }, skip: isBrowser); // https://github.com/flutter/flutter/issues/35347
+
+    test('data.toString', () {
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'fuchsia',
+        'hidUsage': 0x00070004,
+        'codePoint': 97,
+        'character': 'a',
+        'modifiers': 0x10,
+      }).data.toString(), equalsIgnoringHashCodes(
+        'RawKeyEventDataFuchsia#00000(hidUsage: 458756, codePoint: 97, modifiers: 16)'));
+    });
+
+    test('data.equality', () {
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'fuchsia',
+        'hidUsage': 0x00070004,
+        'codePoint': 97,
+        'character': 'a',
+        'modifiers': 0x10,
+      }).data, const RawKeyEventDataFuchsia(
+        hidUsage: 0x00070004,
+        codePoint: 97,
+        modifiers: 0x10,
+      ));
+
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'fuchsia',
+        'hidUsage': 0x00070004,
+        'codePoint': 97,
+        'character': 'a',
+        'modifiers': 0x10,
+      }).data, isNot(equals(const RawKeyEventDataFuchsia())));
+    });
   }, skip: isBrowser); // This is a Fuchsia-specific group.
 
   group('RawKeyEventDataMacOs', () {
@@ -1311,6 +1400,43 @@
       expect(data.physicalKey, equals(PhysicalKeyboardKey.arrowLeft));
       expect(data.logicalKey, equals(LogicalKeyboardKey.arrowLeft));
     }, skip: isBrowser); // https://github.com/flutter/flutter/issues/35347
+
+    test('data.toString', () {
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'macos',
+        'keyCode': 0x00000060,
+        'characters': 'A',
+        'charactersIgnoringModifiers': 'a',
+        'modifiers': 0x10,
+      }).data.toString(), equalsIgnoringHashCodes(
+        'RawKeyEventDataMacOs#00000(characters: A, charactersIgnoringModifiers: a, keyCode: 96, modifiers: 16)'));
+    });
+
+    test('data.equality', () {
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'macos',
+        'keyCode': 0x00000060,
+        'characters': 'A',
+        'charactersIgnoringModifiers': 'a',
+        'modifiers': 0x10,
+      }).data, const RawKeyEventDataMacOs(
+        keyCode: 0x00000060,
+        characters: 'A',
+        charactersIgnoringModifiers: 'a',
+        modifiers: 0x10,
+      ));
+
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'macos',
+        'keyCode': 0x00000060,
+        'characters': 'A',
+        'charactersIgnoringModifiers': 'a',
+        'modifiers': 0x10,
+      }).data, isNot(equals(const RawKeyEventDataMacOs())));
+    });
   }, skip: isBrowser); // This is a macOS-specific group.
 
   group('RawKeyEventDataIos', () {
@@ -1459,6 +1585,43 @@
       expect(data.physicalKey, equals(PhysicalKeyboardKey.arrowLeft));
       expect(data.logicalKey, equals(LogicalKeyboardKey.arrowLeft));
     }, skip: isBrowser); // https://github.com/flutter/flutter/issues/35347
+
+    test('data.toString', () {
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'ios',
+        'keyCode': 0x00000004,
+        'characters': 'A',
+        'charactersIgnoringModifiers': 'a',
+        'modifiers': 0x10,
+      }).data.toString(), equalsIgnoringHashCodes(
+        'RawKeyEventDataIos#00000(characters: A, charactersIgnoringModifiers: a, keyCode: 4, modifiers: 16)'));
+    });
+
+    test('data.equality', () {
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'ios',
+        'keyCode': 0x00000004,
+        'characters': 'A',
+        'charactersIgnoringModifiers': 'a',
+        'modifiers': 0x10,
+      }).data, const RawKeyEventDataIos(
+        keyCode: 0x00000004,
+        characters: 'A',
+        charactersIgnoringModifiers: 'a',
+        modifiers: 0x10,
+      ));
+
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'ios',
+        'keyCode': 0x00000004,
+        'characters': 'A',
+        'charactersIgnoringModifiers': 'a',
+        'modifiers': 0x10,
+      }).data, isNot(equals(const RawKeyEventDataIos())));
+    });
   }, skip: isBrowser); // This is an iOS-specific group.
 
   group('RawKeyEventDataWindows', () {
@@ -1640,9 +1803,46 @@
       expect(lastHandled, true);
       expect(RawKeyboard.instance.keysPressed, isEmpty);
     });
+
+    test('data.toString', () {
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'windows',
+        'keyCode': 0x00000010,
+        'characterCodePoint': 10,
+        'scanCode': 0x0000001e,
+        'modifiers': 0x20,
+      }).data.toString(), equalsIgnoringHashCodes(
+        'RawKeyEventDataWindows#00000(keyCode: 16, scanCode: 30, characterCodePoint: 10, modifiers: 32)'));
+    });
+
+    test('data.equality', () {
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'windows',
+        'keyCode': 0x00000010,
+        'characterCodePoint': 10,
+        'scanCode': 0x0000001e,
+        'modifiers': 0x20,
+      }).data, const RawKeyEventDataWindows(
+        keyCode: 0x00000010,
+        scanCode: 0x1e,
+        modifiers: 0x20,
+        characterCodePoint: 10,
+      ));
+
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'windows',
+        'keyCode': 0x00000010,
+        'characterCodePoint': 10,
+        'scanCode': 0x0000001e,
+        'modifiers': 0x20,
+      }).data, isNot(equals(const RawKeyEventDataWindows())));
+    });
   }, skip: isBrowser); // This is a Windows-specific group.
 
-  group('RawKeyEventDataLinux-GFLW', () {
+  group('RawKeyEventDataLinux-GLFW', () {
     const Map<int, _ModifierCheck> modifierTests = <int, _ModifierCheck>{
       GLFWKeyHelper.modifierAlt: _ModifierCheck(ModifierKey.altModifier, KeyboardSide.all),
       GLFWKeyHelper.modifierShift: _ModifierCheck(ModifierKey.shiftModifier, KeyboardSide.all),
@@ -1828,6 +2028,50 @@
       expect(data.logicalKey, equals(LogicalKeyboardKey.shiftLeft));
       expect(data.keyLabel, isEmpty);
     });
+
+    test('data.toString', () {
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'linux',
+        'toolkit': 'glfw',
+        'keyCode': 65,
+        'scanCode': 0x00000026,
+        'unicodeScalarValues': 0x10FFFF,
+        'modifiers': 0x10,
+      }).data.toString(), equalsIgnoringHashCodes(
+       'RawKeyEventDataLinux#00000(toolkit: GLFW, unicodeScalarValues: 1114111, scanCode: 38, keyCode: 65, modifiers: 16, isDown: true)'));
+    });
+
+    test('data.equality', () {
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'linux',
+        'toolkit': 'glfw',
+        'keyCode': 65,
+        'scanCode': 0x00000026,
+        'unicodeScalarValues': 0x10FFFF,
+        'modifiers': 0x10,
+      }).data, RawKeyEventDataLinux(
+        keyHelper: KeyHelper('glfw'),
+        unicodeScalarValues: 0x10FFFF,
+        keyCode: 65,
+        scanCode: 0x26,
+        modifiers: 0x10,
+        isDown: true,
+      ));
+
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'linux',
+        'toolkit': 'glfw',
+        'keyCode': 65,
+        'scanCode': 0x00000026,
+        'unicodeScalarValues': 0x10FFFF,
+        'modifiers': 0x10,
+      }).data, isNot(equals(RawKeyEventDataLinux(
+        keyHelper: KeyHelper('glfw'), isDown: true)),
+      ));
+    });
   }, skip: isBrowser); // This is a GLFW-specific group.
 
   group('RawKeyEventDataLinux-GTK', () {
@@ -2016,6 +2260,67 @@
       expect(data.logicalKey, equals(LogicalKeyboardKey.shiftLeft));
       expect(data.keyLabel, isEmpty);
     });
+
+    test('data.toString', () {
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'linux',
+        'toolkit': 'gtk',
+        'keyCode': 65,
+        'scanCode': 0x00000026,
+        'unicodeScalarValues': 113,
+        'modifiers': 0x10,
+      }).data.toString(), equalsIgnoringHashCodes(
+        'RawKeyEventDataLinux#00000(toolkit: GTK, unicodeScalarValues: 113, scanCode: 38, keyCode: 65, modifiers: 16, isDown: true)'));
+    });
+
+    test('data.equality', () {
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'linux',
+        'toolkit': 'gtk',
+        'keyCode': 65,
+        'scanCode': 0x00000026,
+        'unicodeScalarValues': 113,
+        'modifiers': 0x10,
+      }).data, RawKeyEventDataLinux(
+        keyHelper: KeyHelper('gtk'),
+        unicodeScalarValues: 113,
+        keyCode: 65,
+        scanCode: 0x26,
+        modifiers: 0x10,
+        isDown: true,
+      ));
+
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'linux',
+        'toolkit': 'gtk',
+        'keyCode': 65,
+        'scanCode': 0x00000026,
+        'unicodeScalarValues': 113,
+        'modifiers': 0x10,
+      }).data, isNot(equals(RawKeyEventDataLinux(
+        keyHelper: KeyHelper('glfw'),
+        unicodeScalarValues: 113,
+        keyCode: 65,
+        scanCode: 0x26,
+        modifiers: 0x10,
+        isDown: true,
+      ))));
+
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'linux',
+        'toolkit': 'gtk',
+        'keyCode': 65,
+        'scanCode': 0x00000026,
+        'unicodeScalarValues': 113,
+        'modifiers': 0x10,
+      }).data, isNot(equals(RawKeyEventDataLinux(
+        keyHelper: KeyHelper('gtk'), isDown: true)),
+      ));
+    });
   }, skip: isBrowser); // This is a GTK-specific group.
 
   group('RawKeyEventDataWeb', () {
@@ -2141,5 +2446,52 @@
       expect(data.logicalKey, equals(LogicalKeyboardKey.arrowDown));
       expect(data.keyLabel, isEmpty);
     });
+
+    test('data.toString', () {
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'web',
+        'code': 'KeyA',
+        'key': 'a',
+        'location': 2,
+        'metaState': 0x10,
+      }).data.toString(), equalsIgnoringHashCodes(
+        'RawKeyEventDataWeb#00000(code: KeyA, key: a, location: 2, metaState: 16)'));
+
+      // Without location
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'web',
+        'code': 'KeyA',
+        'key': 'a',
+        'metaState': 0x10,
+      }).data.toString(), equalsIgnoringHashCodes(
+        'RawKeyEventDataWeb#00000(code: KeyA, key: a, location: 0, metaState: 16)'));
+    });
+
+    test('data.equality', () {
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'web',
+        'code': 'KeyA',
+        'key': 'a',
+        'location': 2,
+        'metaState': 0x10,
+      }).data, const RawKeyEventDataWeb(
+        key: 'a',
+        code: 'KeyA',
+        location: 2,
+        metaState: 0x10,
+      ));
+
+      expect(RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'web',
+        'code': 'KeyA',
+        'key': 'a',
+        'location': 2,
+        'metaState': 0x10,
+      }).data, isNot(equals(const RawKeyEventDataWeb(code: 'KeyA', key: 'a'))));
+    });
   });
 }