From 2aec1dcef1637554d8740d8c458d283b37b6b991 Mon Sep 17 00:00:00 2001 From: Michael Lamers Date: Thu, 21 Nov 2024 15:53:58 +0100 Subject: [PATCH 1/5] fix: don't test for runtimeType equality for object comparison as this breaks type hierarchies like `num` --- lib/src/equatable_utils.dart | 2 -- test/equatable_utils_test.dart | 6 ++++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/src/equatable_utils.dart b/lib/src/equatable_utils.dart index 615b0020..8e65b7cc 100644 --- a/lib/src/equatable_utils.dart +++ b/lib/src/equatable_utils.dart @@ -53,8 +53,6 @@ bool objectsEquals(Object? a, Object? b) { return iterableEquals(a, b); } else if (a is Map && b is Map) { return mapEquals(a, b); - } else if (a?.runtimeType != b?.runtimeType) { - return false; } else if (a != b) { return false; } diff --git a/test/equatable_utils_test.dart b/test/equatable_utils_test.dart index bea9f886..f9182b82 100644 --- a/test/equatable_utils_test.dart +++ b/test/equatable_utils_test.dart @@ -245,6 +245,12 @@ void main() { expect(objectsEquals(bob, alice), isFalse); }); + test('returns true for int and double in a num variable', () { + const num intNum = 0; + const num doubleNum = 0.0; + expect(objectsEquals(intNum, doubleNum), isTrue); + }); + test('returns true for same lists', () { expect(objectsEquals([1, 2, 3], [1, 2, 3]), isTrue); }); From 177ecb9ab6a036250abee6f218ca9fcd374a7102 Mon Sep 17 00:00:00 2001 From: devmil Date: Fri, 22 Nov 2024 22:36:49 +0100 Subject: [PATCH 2/5] test: add tests for Equatable handling without runtimeType check for properties --- test/equatable_utils_test.dart | 124 +++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/test/equatable_utils_test.dart b/test/equatable_utils_test.dart index c5a7955e..e006215f 100644 --- a/test/equatable_utils_test.dart +++ b/test/equatable_utils_test.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:equatable/src/equatable_utils.dart'; +import 'package:meta/meta.dart'; import 'package:test/test.dart' hide equals; class Person with EquatableMixin { @@ -11,10 +12,71 @@ class Person with EquatableMixin { List get props => [name]; } +@immutable +abstract class CustomString { + String get stringValue; + + @override + bool operator ==(Object other) { + if (other is CustomString) { + return other.stringValue == stringValue; + } + if (other is String) { + return other == stringValue; + } + return false; + } + + @override + int get hashCode => stringValue.hashCode; +} + +class PlainString extends CustomString { + PlainString(this.stringValue); + + @override + final String stringValue; +} + +class TranslatableString extends CustomString { + TranslatableString({ + required Map translations, + required String defaultLanguage, + }) : _translations = translations, + _defaultLanguage = defaultLanguage; + + final Map _translations; + final String _defaultLanguage; + + String? inLanguage(String language) => _translations[language]; + @override + String get stringValue => _translations[_defaultLanguage]!; +} + +class Dog with EquatableMixin { + Dog({required this.name}); + + final CustomString name; + + @override + List get props => [name]; +} + +class Cat with EquatableMixin { + Cat({required this.name}); + + final CustomString name; + + @override + List get props => [name]; +} + void main() { final bob = Person(name: 'Bob'); final alice = Person(name: 'Alice'); final aliceCopy = Person(name: 'Alice'); + final fluffyCat = Cat(name: PlainString('fluffy')); + final fluffyDog = Dog(name: PlainString('fluffy')); group('equals', () { test('returns true when both are null', () { @@ -269,6 +331,68 @@ void main() { expect(objectsEquals(bob, alice), isFalse); }); + test( + 'returns false for different Equatable classes ' + 'with same property values', () { + expect(objectsEquals(fluffyDog, fluffyCat), isFalse); + expect(objectsEquals(fluffyCat, fluffyDog), isFalse); + }); + + test('returns true for Equatables with custom equality members ', () { + expect( + objectsEquals( + Dog(name: PlainString('fluffy')), + Dog(name: PlainString('fluffy')), + ), + isTrue, + ); + }); + + test( + 'returns true for Equatables with custom equality members ' + 'that are equal but have a different runtimeType', () { + final plainName = PlainString('fluffy'); + final translatableName = TranslatableString( + translations: const { + 'en': 'fluffy', + 'de': 'flauschi', + }, + defaultLanguage: 'en', + ); + //cross check + expect(plainName == translatableName, isTrue); + //actual check + expect( + objectsEquals( + Dog(name: plainName), + Dog(name: translatableName), + ), + isTrue, + ); + }); + test( + 'returns false for Equatables with custom equality members ' + 'that are not equal and have a different runtimeType', () { + final plainName = PlainString('fluffy'); + final translatableName = TranslatableString( + translations: const { + 'en': 'fluffy', + 'de': 'flauschi', + }, + defaultLanguage: 'de', + ); + //cross check + expect(plainName == translatableName, isFalse); + //actual check + expect( + objectsEquals( + Dog(name: plainName), + Dog(name: translatableName), + ), + isFalse, + ); + }); + test('returns true for int and double in a num variable', () { const num intNum = 0; const num doubleNum = 0.0; From 5e1a53c8f9e26c2e33e3ddd420572d3d0ea1c12a Mon Sep 17 00:00:00 2001 From: devmil Date: Fri, 22 Nov 2024 22:48:01 +0100 Subject: [PATCH 3/5] chore: removes redundant test --- test/equatable_utils_test.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/equatable_utils_test.dart b/test/equatable_utils_test.dart index e006215f..9ac1fd58 100644 --- a/test/equatable_utils_test.dart +++ b/test/equatable_utils_test.dart @@ -393,12 +393,6 @@ void main() { ); }); - test('returns true for int and double in a num variable', () { - const num intNum = 0; - const num doubleNum = 0.0; - expect(objectsEquals(intNum, doubleNum), isTrue); - }); - test('returns true for same lists', () { expect(objectsEquals([1, 2, 3], [1, 2, 3]), isTrue); }); From 2673908301910b0507317bd2a77f430b89202c66 Mon Sep 17 00:00:00 2001 From: devmil Date: Sun, 24 Nov 2024 13:32:36 +0100 Subject: [PATCH 4/5] test: much better example for custom equality operator in the test --- test/equatable_utils_test.dart | 90 ++++++++++++++++------------------ 1 file changed, 42 insertions(+), 48 deletions(-) diff --git a/test/equatable_utils_test.dart b/test/equatable_utils_test.dart index 9ac1fd58..65ba6994 100644 --- a/test/equatable_utils_test.dart +++ b/test/equatable_utils_test.dart @@ -13,50 +13,52 @@ class Person with EquatableMixin { } @immutable -abstract class CustomString { - String get stringValue; +abstract class AnimalName { + const AnimalName(); + + String get normalized; @override bool operator ==(Object other) { - if (other is CustomString) { - return other.stringValue == stringValue; - } - if (other is String) { - return other == stringValue; + if (other is AnimalName) { + return normalized == other.normalized; } return false; } @override - int get hashCode => stringValue.hashCode; + int get hashCode => normalized.hashCode; } -class PlainString extends CustomString { - PlainString(this.stringValue); +class SimpleName extends AnimalName { + const SimpleName(this.name); + + final String name; @override - final String stringValue; + String get normalized => name.replaceAll(' ', '').toLowerCase(); } -class TranslatableString extends CustomString { - TranslatableString({ - required Map translations, - required String defaultLanguage, - }) : _translations = translations, - _defaultLanguage = defaultLanguage; +class PedigreeName extends AnimalName { + const PedigreeName({ + required this.prefix, + required this.name, + required this.suffix, + }); - final Map _translations; - final String _defaultLanguage; + final String prefix; + final String name; + final String suffix; - String? inLanguage(String language) => _translations[language]; @override - String get stringValue => _translations[_defaultLanguage]!; + String get normalized => + '$prefix$name$suffix'.replaceAll(' ', '').toLowerCase(); } class Dog with EquatableMixin { Dog({required this.name}); - final CustomString name; + final AnimalName name; @override List get props => [name]; @@ -65,7 +67,7 @@ class Dog with EquatableMixin { class Cat with EquatableMixin { Cat({required this.name}); - final CustomString name; + final AnimalName name; @override List get props => [name]; @@ -75,8 +77,8 @@ void main() { final bob = Person(name: 'Bob'); final alice = Person(name: 'Alice'); final aliceCopy = Person(name: 'Alice'); - final fluffyCat = Cat(name: PlainString('fluffy')); - final fluffyDog = Dog(name: PlainString('fluffy')); + final fluffyCat = Cat(name: const SimpleName('fluffy')); + final fluffyDog = Dog(name: const SimpleName('fluffy')); group('equals', () { test('returns true when both are null', () { @@ -341,8 +343,8 @@ void main() { test('returns true for Equatables with custom equality members ', () { expect( objectsEquals( - Dog(name: PlainString('fluffy')), - Dog(name: PlainString('fluffy')), + Dog(name: const SimpleName('fluffy')), + Dog(name: const SimpleName('fluffy')), ), isTrue, ); @@ -351,21 +353,15 @@ void main() { test( 'returns true for Equatables with custom equality members ' 'that are equal but have a different runtimeType', () { - final plainName = PlainString('fluffy'); - final translatableName = TranslatableString( - translations: const { - 'en': 'fluffy', - 'de': 'flauschi', - }, - defaultLanguage: 'en', - ); + const nameSimple = SimpleName('fluffy'); + const namePedigree = PedigreeName(prefix: '', name: 'Fluffy', suffix: ''); //cross check - expect(plainName == translatableName, isTrue); + expect(nameSimple == namePedigree, isTrue); //actual check expect( objectsEquals( - Dog(name: plainName), - Dog(name: translatableName), + Dog(name: nameSimple), + Dog(name: namePedigree), ), isTrue, ); @@ -373,21 +369,19 @@ void main() { test( 'returns false for Equatables with custom equality members ' 'that are not equal and have a different runtimeType', () { - final plainName = PlainString('fluffy'); - final translatableName = TranslatableString( - translations: const { - 'en': 'fluffy', - 'de': 'flauschi', - }, - defaultLanguage: 'de', + const nameSimple = SimpleName('fluffy'); + const differentNamePedigree = PedigreeName( + prefix: 'Sir ', + name: 'Fluffy', + suffix: ' from Midgard', ); //cross check - expect(plainName == translatableName, isFalse); + expect(nameSimple == differentNamePedigree, isFalse); //actual check expect( objectsEquals( - Dog(name: plainName), - Dog(name: translatableName), + Dog(name: nameSimple), + Dog(name: differentNamePedigree), ), isFalse, ); From f48ea88e61eda1546ef5b0decca2023417936103 Mon Sep 17 00:00:00 2001 From: Michael Lamers Date: Thu, 28 Nov 2024 12:08:51 +0100 Subject: [PATCH 5/5] adds a test that works on 2.0.5 and breaks on 2.0.7 regarding custom equality --- test/equatable_utils_test.dart | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/equatable_utils_test.dart b/test/equatable_utils_test.dart index 65ba6994..21b21bc4 100644 --- a/test/equatable_utils_test.dart +++ b/test/equatable_utils_test.dart @@ -366,6 +366,7 @@ void main() { isTrue, ); }); + test( 'returns false for Equatables with custom equality members ' 'that are not equal and have a different runtimeType', () { @@ -391,6 +392,23 @@ void main() { expect(objectsEquals([1, 2, 3], [1, 2, 3]), isTrue); }); + test( + 'returns true for List of custom equality objects ' + 'that are equal but have a different runtimeType', () { + const nameSimple = SimpleName('fluffy'); + const namePedigree = PedigreeName(prefix: '', name: 'Fluffy', suffix: ''); + //cross check + expect(nameSimple == namePedigree, isTrue); + //actual check + expect( + objectsEquals( + [nameSimple], + [namePedigree], + ), + isTrue, + ); + }); + test('returns true for same sets', () { expect(objectsEquals({1, 2, 3}, {1, 2, 3}), isTrue); expect(objectsEquals({1, 3, 2}, {1, 2, 3}), isTrue);