diff --git a/CHANGELOG.md b/CHANGELOG.md index ac47188..b3016ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.3.1 + +- Added the `isNullOrEmpty` and `isNotNullOrEmpty` extensions for `List` objects. +- Fixed a bug in the `Ticker` extension that prevented intervals shorter than 1 second from being used. +- [chore] Added unit tests for all extensions and utilities in the package. + ## 1.3.0 - Added a non-breaking space character to `CommonString` as `CommonString.nbsp` diff --git a/README.md b/README.md index 8bcf256..8a2a129 100644 --- a/README.md +++ b/README.md @@ -354,18 +354,40 @@ The following extensions have been added to the `List` object: new `List` or filter the existing list by specifying the `inplace` option. ```dart - final list = [1, 2, 2, 3, 4, 4]; - final uniqueList = list.unique(); + const List list = [1, 2, 2, 3, 4, 4]; + final List uniqueList = list.unique(); print(uniqueList); // Output: [1, 2, 3, 4] - final people = [ + const List people = [ Person(id: 1, name: 'Alice'), Person(id: 2, name: 'Bob'), Person(id: 1, name: 'Alice Duplicate'), ]; - final uniquePeople = people.unique((person) => person.id); + final List uniquePeople = people.unique((person) => person.id); print(uniquePeople.map((p) => p.name)); // Output: ['Alice', 'Bob'] ``` +- `isNullOrEmpty`: Checks if a list is either null or empty. + + ```dart + const List list = [1, 2, 3]; + print(list.isNullOrEmpty); // Output: false + final List emptyList = []; + print(emptyList.isNullOrEmpty); // Output: true + final List? nullList = null; + print(nullList.isNullOrEmpty); // Output: true + ``` + +- `isNullOrEmpty`: Checks if a list is either null or empty. + + ```dart + final List list = [1, 2, 3]; + print(list.isNullOrEmpty); // Output: false + final List emptyList = []; + print(emptyList.isNullOrEmpty); // Output: true + final List? nullList = null; + print(nullList.isNullOrEmpty); // Output: true + ``` + ## Contributing Contributions are welcome! Feel free to fork the repository and submit pull diff --git a/lib/src/extensions/list.dart b/lib/src/extensions/list.dart index e06225b..27c20b0 100644 --- a/lib/src/extensions/list.dart +++ b/lib/src/extensions/list.dart @@ -44,3 +44,41 @@ extension Unique on List { return list; } } + +/// An extension on nullable `List` to provide convenience methods for checking nullability and emptiness. +/// +/// This extension adds getters that make it easier to check if a list is null, +/// empty, or both in a single operation. +extension ListNullability on List? { + /// Returns `true` if the list is either not null and not empty. + /// + /// This is the inverse of [isNullOrEmpty]. + /// + /// Example usage: + /// ```dart + /// List? list = [1, 2, 3]; + /// print(list.isNotNullOrEmpty); // Output: true + /// + /// list = []; + /// print(list.isNotNullOrEmpty); // Output: false + /// + /// list = null; + /// print(list.isNotNullOrEmpty); // Output: false + /// ``` + bool get isNotNullOrEmpty => !isNullOrEmpty; + + /// Returns `true` if the list is either null or empty. + /// + /// Example usage: + /// ```dart + /// List? list = null; + /// print(list.isNullOrEmpty); // Output: true + /// + /// list = []; + /// print(list.isNullOrEmpty); // Output: true + /// + /// list = [1, 2, 3]; + /// print(list.isNullOrEmpty); // Output: false + /// ``` + bool get isNullOrEmpty => this == null || this!.isEmpty; +} diff --git a/lib/src/utils/ticker.dart b/lib/src/utils/ticker.dart index a125412..8836b77 100644 --- a/lib/src/utils/ticker.dart +++ b/lib/src/utils/ticker.dart @@ -27,7 +27,7 @@ class Ticker { required Duration timeout, Duration interval = const Duration(seconds: 1), }) { - final int ticks = timeout.inSeconds ~/ interval.inSeconds; + final int ticks = timeout.inMicroseconds ~/ interval.inMicroseconds; return Stream.periodic(interval, (x) => ticks - x - 1).take(ticks); } } diff --git a/pubspec.yaml b/pubspec.yaml index c1eaa21..520d178 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: arcane_helper_utils description: Provides a variety of helpful utilities and extensions for Flutter and Dart. -version: 1.3.0 +version: 1.3.1 repository: https://github.com/hanskokx/arcane_helper_utils issue_tracker: https://github.com/hanskokx/arcane_helper_utils/issues @@ -19,3 +19,4 @@ dependencies: dev_dependencies: arcane_analysis: ^1.0.3 + test: any diff --git a/test/extensions/date_time_test.dart b/test/extensions/date_time_test.dart new file mode 100644 index 0000000..6a8354d --- /dev/null +++ b/test/extensions/date_time_test.dart @@ -0,0 +1,100 @@ +import "package:arcane_helper_utils/arcane_helper_utils.dart"; +import "package:test/test.dart"; + +void main() { + group("StartAndEndOfPeriod", () { + test("startOfHour returns correct DateTime", () { + final dateTime = DateTime(2023, 1, 1, 12, 30, 45); + final result = dateTime.startOfHour; + expect(result, DateTime(2023, 1, 1, 12, 0, 0)); + }); + + test("endOfHour returns correct DateTime", () { + final dateTime = DateTime(2023, 1, 1, 12, 30, 45); + final result = dateTime.endOfHour; + expect(result, DateTime(2023, 1, 1, 12, 59, 59, 999, 999)); + }); + + test("startOfDay returns correct DateTime", () { + final dateTime = DateTime(2023, 1, 1, 12, 30, 45); + final result = dateTime.startOfDay; + expect(result, DateTime(2023, 1, 1)); + }); + + test("endOfDay returns correct DateTime", () { + final dateTime = DateTime(2023, 1, 1, 12, 30, 45); + final result = dateTime.endOfDay; + expect(result, DateTime(2023, 1, 1, 23, 59, 59, 999, 999)); + }); + }); + + group("DaysInMonth", () { + test("returns correct days for regular months", () { + expect(DateTime(2023, 1, 1).daysInMonth, 31); // January + expect(DateTime(2023, 3, 1).daysInMonth, 31); // March + expect(DateTime(2023, 4, 1).daysInMonth, 30); // April + expect(DateTime(2023, 5, 1).daysInMonth, 31); // May + expect(DateTime(2023, 6, 1).daysInMonth, 30); // June + expect(DateTime(2023, 7, 1).daysInMonth, 31); // July + expect(DateTime(2023, 8, 1).daysInMonth, 31); // August + expect(DateTime(2023, 9, 1).daysInMonth, 30); // September + expect(DateTime(2023, 10, 1).daysInMonth, 31); // October + expect(DateTime(2023, 11, 1).daysInMonth, 30); // November + expect(DateTime(2023, 12, 1).daysInMonth, 31); // December + }); + + test("returns correct days for February in leap year", () { + expect(DateTime(2020, 2, 1).daysInMonth, 29); + }); + + test("returns correct days for February in non-leap year", () { + expect(DateTime(2023, 2, 1).daysInMonth, 28); + }); + }); + + group("IsToday", () { + test("returns true for current date", () { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + expect(today.isToday, true); + }); + + test("returns false for past date", () { + final yesterday = DateTime.now().subtract(const Duration(days: 1)); + expect(yesterday.isToday, false); + }); + + test("returns false for future date", () { + final tomorrow = DateTime.now().add(const Duration(days: 1)); + expect(tomorrow.isToday, false); + }); + }); + + group("YesterdayAndTomorrow", () { + test("yesterday returns one day before current date", () { + final now = DateTime.now(); + final expectedYesterday = DateTime(now.year, now.month, now.day - 1); + expect(DateTime.now().yesterday, expectedYesterday); + }); + + test("yesterday handles start of month correctly", () { + final now = DateTime.now(); + final expected = DateTime(now.year, now.month, now.day) + .subtract(const Duration(days: 1)); + expect(DateTime.now().yesterday, expected); + }); + + test("tomorrow returns one day after current date", () { + final now = DateTime.now(); + final expectedTomorrow = DateTime(now.year, now.month, now.day + 1); + expect(DateTime.now().tomorrow, expectedTomorrow); + }); + + test("tomorrow handles end of month correctly", () { + final now = DateTime.now(); + final expected = + DateTime(now.year, now.month, now.day).add(const Duration(days: 1)); + expect(DateTime.now().tomorrow, expected); + }); + }); +} diff --git a/test/extensions/dynamic_test.dart b/test/extensions/dynamic_test.dart new file mode 100644 index 0000000..7c03435 --- /dev/null +++ b/test/extensions/dynamic_test.dart @@ -0,0 +1,77 @@ +import "dart:async"; + +import "package:arcane_helper_utils/arcane_helper_utils.dart"; +import "package:test/test.dart"; + +void main() { + group("DynamicPrintExtension", () { + late List printLog; + late ZoneSpecification spec; + late Zone testZone; + + setUp(() { + // Create a buffer to store print output + printLog = []; + + // Create a custom zone specification that captures print output + spec = ZoneSpecification( + print: (_, __, ___, String msg) { + printLog.add(msg); + }, + ); + + // Create a test zone with the custom specification + testZone = Zone.current.fork(specification: spec); + }); + + test("printValue prints and returns the value without label", () { + testZone.run(() { + const testValue = "test string"; + // Ignore deprecation warning in test + // ignore: deprecated_member_use_from_same_package + final result = testValue.printValue(); + + expect(result, equals(testValue)); + expect(printLog, hasLength(1)); + expect(printLog.first, equals(testValue)); + }); + }); + + test("printValue prints and returns the value with label", () { + testZone.run(() { + const testValue = 42; + const label = "number"; + // ignore: deprecated_member_use_from_same_package + final result = testValue.printValue(label); + + expect(result, equals(testValue)); + expect(printLog, hasLength(1)); + expect(printLog.first, equals("$label: $testValue")); + }); + }); + + test("printValue works with null values", () { + testZone.run(() { + const String? testValue = null; + // ignore: deprecated_member_use_from_same_package + final result = testValue.printValue("nullable"); + + expect(result, isNull); + expect(printLog, hasLength(1)); + expect(printLog.first, equals("nullable: null")); + }); + }); + + test("printValue works with complex objects", () { + testZone.run(() { + final testValue = {"key": "value"}; + // ignore: deprecated_member_use_from_same_package + final result = testValue.printValue>(); + + expect(result, equals(testValue)); + expect(printLog, hasLength(1)); + expect(printLog.first, equals(testValue.toString())); + }); + }); + }); +} diff --git a/test/extensions/list_test.dart b/test/extensions/list_test.dart new file mode 100644 index 0000000..27b06f0 --- /dev/null +++ b/test/extensions/list_test.dart @@ -0,0 +1,59 @@ +import "package:arcane_helper_utils/arcane_helper_utils.dart"; +import "package:test/test.dart"; + +void main() { + group("Unique", () { + test("removes duplicates from simple list", () { + final list = [1, 2, 2, 3, 3, 3]; + expect(list.unique(), [1, 2, 3]); + }); + + test("removes duplicates using custom identifier", () { + final list = [ + {"id": 1, "name": "John"}, + {"id": 2, "name": "Jane"}, + {"id": 1, "name": "John Copy"}, + ]; + final result = list.unique((item) => item["id"]); + expect(result.length, 2); + expect(result.map((e) => e["id"]).toList(), [1, 2]); + }); + + test("handles empty list", () { + final List list = []; + expect(list.unique(), []); + }); + + test("preserves order of elements", () { + final list = [3, 1, 2, 1, 3]; + expect(list.unique(), [3, 1, 2]); + }); + }); + + group("ListNullability", () { + test("isNullOrEmpty returns true for null list", () { + List? list; + expect(list.isNullOrEmpty, true); + }); + + test("isNullOrEmpty returns true for empty list", () { + final List list = []; + expect(list.isNullOrEmpty, true); + }); + + test("isNullOrEmpty returns false for non-empty list", () { + final list = [1, 2, 3]; + expect(list.isNullOrEmpty, false); + }); + + test("isNotNullOrEmpty returns opposite of isNullOrEmpty", () { + List? nullList; + final emptyList = []; + final nonEmptyList = [1, 2, 3]; + + expect(nullList.isNotNullOrEmpty, false); + expect(emptyList.isNotNullOrEmpty, false); + expect(nonEmptyList.isNotNullOrEmpty, true); + }); + }); +} diff --git a/test/extensions/string_jwt_test.dart b/test/extensions/string_jwt_test.dart new file mode 100644 index 0000000..752520f --- /dev/null +++ b/test/extensions/string_jwt_test.dart @@ -0,0 +1,40 @@ +import "package:arcane_helper_utils/arcane_helper_utils.dart"; +import "package:test/test.dart"; + +void main() { + // Example JWT token for testing + const String validToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiJ0ZXN0QGV4YW1wbGUuY29tIiwiZXhwIjoxNzM1Njg5NjAwLCJ1aWQiOiIxMjM0NTYifQ." + "signature"; + + group("JWTUtility", () { + test("jwtEmail extracts email correctly", () { + expect(validToken.jwtEmail(), "test@example.com"); + }); + + test("jwtEmail returns null for invalid token", () { + expect("invalid.token".jwtEmail(), null); + expect("".jwtEmail(), null); + }); + + test("jwtExpiryTime extracts expiry time correctly", () { + final DateTime? expiry = validToken.jwtExpiryTime(); + expect(expiry, isNotNull); + expect(expiry?.year, 2025); // Based on the exp value in the test token + }); + + test("jwtExpiryTime returns null for invalid token", () { + expect("invalid.token".jwtExpiryTime(), null); + expect("".jwtExpiryTime(), null); + }); + + test("jwtUserId extracts user ID correctly", () { + expect(validToken.jwtUserId(), "123456"); + }); + + test("jwtUserId returns null for invalid token", () { + expect("invalid.token".jwtUserId(), null); + expect("".jwtUserId(), null); + }); + }); +} diff --git a/test/extensions/string_test.dart b/test/extensions/string_test.dart new file mode 100644 index 0000000..81c6119 --- /dev/null +++ b/test/extensions/string_test.dart @@ -0,0 +1,75 @@ +import "package:arcane_helper_utils/arcane_helper_utils.dart"; +import "package:test/test.dart"; + +void main() { + group("CommonString", () { + test("contains correct constant values", () { + expect(CommonString.emDash, "—"); + expect(CommonString.bulletPoint, "•"); + expect(CommonString.nbsp, "\u00A0"); + }); + }); + + group("String Nullability", () { + test("isNotNullOrEmpty returns correct values", () { + String? nullString; + expect(nullString.isNotNullOrEmpty, false); + expect("".isNotNullOrEmpty, false); + expect(" ".isNotNullOrEmpty, false); + expect("text".isNotNullOrEmpty, true); + }); + + test("isNullOrEmpty returns correct values", () { + String? nullString; + expect(nullString.isNullOrEmpty, true); + expect("".isNullOrEmpty, true); + expect(" ".isNullOrEmpty, true); + expect("text".isNullOrEmpty, false); + }); + }); + + group("String Split", () { + test("splitByLength splits string correctly", () { + expect("abcdef".splitByLength(2), ["ab", "cd", "ef"]); + expect("abcde".splitByLength(2), ["ab", "cd", "e"]); + expect("abcd".splitByLength(4), ["abcd"]); + expect("abc".splitByLength(4), ["abc"]); + expect("".splitByLength(2), []); + }); + + test("handles edge cases", () { + expect("a".splitByLength(1), ["a"]); + expect("abc".splitByLength(10), ["abc"]); + expect("\u00A0".splitByLength(1), ["\u00A0"]); + }); + }); + + group("String TextManipulation", () { + test("capitalize handles various cases", () { + expect("hello".capitalize, "Hello"); + expect("HELLO".capitalize, "Hello"); + expect("h".capitalize, "H"); + expect("".capitalize, null); + String? nullString; + expect(nullString.capitalize, null); + }); + + test("capitalizeWords handles various cases", () { + expect("hello world".capitalizeWords, "Hello World"); + expect("HELLO WORLD".capitalizeWords, "Hello World"); + expect("hello".capitalizeWords, "Hello"); + expect("".capitalizeWords, null); + String? nullString; + expect(nullString.capitalizeWords, null); + }); + + test("spacePascalCase handles various cases", () { + expect("HelloWorld".spacePascalCase, "Hello World"); + expect("ABC".spacePascalCase, "A B C"); + expect("helloWorld".spacePascalCase, "hello World"); + expect("".spacePascalCase, null); + String? nullString; + expect(nullString.spacePascalCase, null); + }); + }); +} diff --git a/test/utils/json_converter_test.dart b/test/utils/json_converter_test.dart new file mode 100644 index 0000000..4317700 --- /dev/null +++ b/test/utils/json_converter_test.dart @@ -0,0 +1,51 @@ +import "package:arcane_helper_utils/arcane_helper_utils.dart"; +import "package:test/test.dart"; + +void main() { + group("DoubleConverter", () { + const converter = DoubleConverter(); + + test("fromJson converts valid string to double", () { + expect(converter.fromJson("123.45"), 123.45); + expect(converter.fromJson("-123.45"), -123.45); + expect(converter.fromJson("0.0"), 0.0); + }); + + test("fromJson handles null and invalid inputs", () { + expect(converter.fromJson(null), null); + expect(converter.fromJson(""), null); + expect(converter.fromJson("invalid"), null); + }); + + test("toJson converts double to string", () { + expect(converter.toJson(123.45), "123.45"); + expect(converter.toJson(-123.45), "-123.45"); + expect(converter.toJson(0.0), "0.0"); + expect(converter.toJson(null), null); + }); + }); + + group("IntegerConverter", () { + const converter = IntegerConverter(); + + test("fromJson converts valid string to int", () { + expect(converter.fromJson("123"), 123); + expect(converter.fromJson("-123"), -123); + expect(converter.fromJson("0"), 0); + }); + + test("fromJson handles null and invalid inputs", () { + expect(converter.fromJson(null), null); + expect(converter.fromJson(""), null); + expect(converter.fromJson("invalid"), null); + expect(converter.fromJson("123.45"), null); + }); + + test("toJson converts int to string", () { + expect(converter.toJson(123), "123"); + expect(converter.toJson(-123), "-123"); + expect(converter.toJson(0), "0"); + expect(converter.toJson(null), null); + }); + }); +} diff --git a/test/utils/ticker_test.dart b/test/utils/ticker_test.dart new file mode 100644 index 0000000..1bedbdc --- /dev/null +++ b/test/utils/ticker_test.dart @@ -0,0 +1,71 @@ +import "package:arcane_helper_utils/arcane_helper_utils.dart"; +import "package:test/test.dart"; + +void main() { + group("Ticker", () { + test("no ticks emitted when interval is null and timeout is less than 1s", + () async { + const ticker = Ticker(); + final stream = ticker.tick( + timeout: const Duration(milliseconds: 300), + ); + + final List emitted = await stream.toList(); + expect(emitted, isEmpty); + }); + + test("one tick emitted when interval is null and timeout is 1s", () async { + const ticker = Ticker(); + final stream = ticker.tick( + timeout: const Duration(seconds: 1), + ); + + final List emitted = await stream.toList(); + expect(emitted, [0]); + }); + + test("multiple ticks emitted when interval is null and timeout >1s", + () async { + const ticker = Ticker(); + final stream = ticker.tick( + timeout: const Duration(seconds: 3), + ); + + final List emitted = await stream.toList(); + expect(emitted, [2, 1, 0]); + }); + + test("respects custom interval", () async { + const ticker = Ticker(); + final stream = ticker.tick( + timeout: const Duration(milliseconds: 400), + interval: const Duration(milliseconds: 200), + ); + + final List emitted = await stream.toList(); + expect(emitted, [1, 0]); + }); + + test("handles zero timeout", () async { + const ticker = Ticker(); + final stream = ticker.tick( + timeout: Duration.zero, + interval: const Duration(seconds: 1), + ); + + final List emitted = await stream.toList(); + expect(emitted, isEmpty); + }); + + test("handles timeout less than interval", () async { + const ticker = Ticker(); + final stream = ticker.tick( + timeout: const Duration(milliseconds: 500), + interval: const Duration(seconds: 1), + ); + + final List emitted = await stream.toList(); + expect(emitted, isEmpty); + }); + }); +}