Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
920fe0d
:white_check_mark: add unit tests for ListFormat generators
techouse Sep 26, 2025
49eca19
:white_check_mark: add unit tests for Utils.merge, encode, and helper…
techouse Sep 26, 2025
162cdd7
:white_check_mark: add test for legacy decoder fallback in DecodeOptions
techouse Sep 26, 2025
f3e2e4d
:recycle: improve boundary string construction in utils_additional_test
techouse Sep 26, 2025
4b48728
:white_check_mark: add test for deprecated decoder in DecodeOptions
techouse Sep 26, 2025
1731596
:white_check_mark: add targeted unit tests for QS.decode edge cases
techouse Sep 26, 2025
3665669
:white_check_mark: add targeted unit tests for QS.decode edge cases
techouse Sep 26, 2025
d30db5f
:white_check_mark: add tests for Utils.merge and encode surrogate han…
techouse Sep 26, 2025
fb64587
:white_check_mark: add test for merging maps with scalar targets in U…
techouse Sep 26, 2025
3eb8a1e
:recycle: refactor encode logic to handle dot encoding for root primi…
techouse Sep 26, 2025
faf59cd
:white_check_mark: add tests for decode depth remainder wrapping scen…
techouse Sep 26, 2025
e42be8f
:white_check_mark: add tests for encoding edge cases in QS.encode
techouse Sep 26, 2025
a4d8cae
:white_check_mark: add additional tests for QS.encode covering empty …
techouse Sep 26, 2025
4775b94
:white_check_mark: add tests for surrogate and charset edge cases in …
techouse Sep 26, 2025
5d8c95e
:rewind: revert changes
techouse Sep 26, 2025
f90c1db
:white_check_mark: add tests for MapBase implementation with throwing…
techouse Sep 26, 2025
62e82a0
:white_check_mark: update test for QS.encode to allow flexible encodi…
techouse Sep 26, 2025
e5b089d
:pencil2: change return type of operator [] to non-nullable String in…
techouse Sep 26, 2025
64bca00
:white_check_mark: update tests in encode_edge_cases_test.dart to use…
techouse Sep 26, 2025
7e11b30
:white_check_mark: update tests in encode_edge_cases_test.dart to ver…
techouse Sep 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions test/unit/decode_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2792,4 +2792,84 @@ void main() {
);
});
});

group('Targeted coverage additions', () {
test('comma splitting truncates to remaining list capacity', () {
final result = QS.decode(
'a=1,2,3',
const DecodeOptions(comma: true, listLimit: 2),
);

final Iterable<dynamic> iterable = result['a'] as Iterable;
expect(iterable.toList(), equals(['1', '2']));
});

test('comma splitting throws when limit exceeded in strict mode', () {
expect(
() => QS.decode(
'a=1,2',
const DecodeOptions(
comma: true,
listLimit: 1,
throwOnLimitExceeded: true,
),
),
throwsA(isA<RangeError>()),
);
});

test('strict depth throws when additional bracket groups remain', () {
expect(
() => QS.decode(
'a[b][c][d]=1',
const DecodeOptions(depth: 2, strictDepth: true),
),
throwsA(isA<RangeError>()),
);
});

test('non-strict depth keeps remainder as literal bracket segment', () {
final decoded = QS.decode(
'a[b][c][d]=1',
const DecodeOptions(depth: 2),
);

expect(
decoded,
equals({
'a': {
'b': {
'c': {'[d]': '1'}
}
}
}));
});

test('parameterLimit < 1 coerces to zero and triggers argument error', () {
expect(
() => QS.decode(
'a=b',
const DecodeOptions(parameterLimit: 0.5),
),
throwsA(isA<ArgumentError>()),
);
});

test('allowDots accepts hyphen-prefixed segments as identifiers', () {
expect(
QS.decode('a.-foo=1', const DecodeOptions(allowDots: true)),
equals({
'a': {'-foo': '1'}
}),
);
});

test('allowDots keeps literal dot when segment start is not identifier',
() {
expect(
QS.decode('a.@foo=1', const DecodeOptions(allowDots: true)),
equals({'a.@foo': '1'}),
);
});
});
}
65 changes: 16 additions & 49 deletions test/unit/encode_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ import '../fixtures/dummy_enum.dart';

// Custom class that is neither a Map nor an Iterable
class CustomObject {
final String value;

CustomObject(this.value);

String? operator [](String key) => key == 'prop' ? value : null;
final String value;

String? operator [](String key) {
if (key == 'prop') return value;
throw UnsupportedError('Only prop supported');
}
}

void main() {
Expand Down Expand Up @@ -91,55 +94,19 @@ void main() {
result2, 'dates=2023-01-01T00:00:00.000Z,2023-01-01T00:00:00.000Z');
});

test('Access property of non-Map, non-Iterable object', () {
// This test targets line 161 in encode.dart
// Create a custom object that's neither a Map nor an Iterable
test('filter callback can expand custom objects into maps', () {
final customObj = CustomObject('test');

// Create a test that will try to access a property of the custom object
// We need to modify our approach to ensure the code path is exercised

// First, let's verify that our CustomObject works as expected
expect(customObj['prop'], equals('test'));

// Now, let's create a test that will try to access the property
// We'll use a different approach that's more likely to exercise the code path
try {
final result = QS.encode(
{'obj': customObj},
const EncodeOptions(encode: false),
);

// The result might be empty, but the important thing is that the code path is executed
expect(result.isEmpty, isTrue);
} catch (e) {
// If an exception is thrown, that's also fine as long as the code path is executed
// We're just trying to increase coverage, not test functionality
}

// Try another approach with a custom filter
try {
final result = QS.encode(
{'obj': customObj},
EncodeOptions(
encode: false,
filter: (prefix, value) {
// This should trigger the code path that accesses properties of non-Map, non-Iterable objects
if (value is CustomObject) {
return value['prop'];
}
return value;
},
),
);
final result = QS.encode(
{'obj': customObj},
EncodeOptions(
encode: false,
filter: (prefix, value) =>
value is CustomObject ? {'prop': value.value} : value,
),
);

// The result might vary, but the important thing is that the code path is executed
// Check if the result contains the expected value
expect(result, contains('obj=test'));
} catch (e) {
// If an exception is thrown, that's also fine as long as the code path is executed
// Exception: $e
}
expect(result, equals('obj[prop]=test'));
});
test('encodes a query string map', () {
expect(QS.encode({'a': 'b'}), equals('a=b'));
Expand Down
26 changes: 26 additions & 0 deletions test/unit/list_format_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'package:qs_dart/qs_dart.dart';
import 'package:test/test.dart';

void main() {
group('ListFormat generators', () {
test('brackets format appends empty brackets', () {
expect(ListFormat.brackets.generator('foo'), equals('foo[]'));
});

test('comma format keeps prefix untouched', () {
expect(ListFormat.comma.generator('foo'), equals('foo'));
});

test('repeat format reuses the prefix', () {
expect(ListFormat.repeat.generator('foo'), equals('foo'));
});

test('indices format injects the element index', () {
expect(ListFormat.indices.generator('foo', '2'), equals('foo[2]'));
});

test('toString mirrors enum name', () {
expect(ListFormat.indices.toString(), equals('indices'));
});
});
}
30 changes: 30 additions & 0 deletions test/unit/models/decode_options_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -332,4 +332,34 @@ void main() {
)));
});
});

group('DecodeOptions legacy decoder fallback', () {
test('prefers legacy decoder when primary decoder absent', () {
final calls = <Map<String, Object?>>[];
final opts = DecodeOptions(
legacyDecoder: (String? value, {Encoding? charset}) {
calls.add({'value': value, 'charset': charset});
return value?.toUpperCase();
},
);

expect(opts.decode('abc', charset: latin1), equals('ABC'));
expect(calls, hasLength(1));
expect(calls.single['charset'], equals(latin1));
});

test('deprecated decoder forwards to decode implementation', () {
final opts = DecodeOptions(
decoder: (String? value, {Encoding? charset, DecodeKind? kind}) =>
'kind=$kind,value=$value,charset=$charset',
);

expect(
opts.decoder('foo', charset: latin1, kind: DecodeKind.key),
equals(
'kind=${DecodeKind.key},value=foo,charset=Instance of \'Latin1Codec\'',
),
);
});
});
}
117 changes: 117 additions & 0 deletions test/unit/utils_additional_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import 'dart:collection';

import 'package:qs_dart/qs_dart.dart';
import 'package:qs_dart/src/utils.dart';
import 'package:test/test.dart';

void main() {
group('Utils.merge edge branches', () {
test('normalizes to map when Undefined persists and parseLists is false',
() {
final result = Utils.merge(
[const Undefined()],
const [Undefined()],
const DecodeOptions(parseLists: false),
);

final splay = result as SplayTreeMap;
expect(splay.isEmpty, isTrue);
});

test('combines non-iterable scalars into a list pair', () {
expect(Utils.merge('left', 'right'), equals(['left', 'right']));
});

test('combines scalar and iterable respecting Undefined stripping', () {
final result = Utils.merge(
'seed',
['tail', const Undefined()],
);
expect(result, equals(['seed', 'tail']));
});

test('wraps custom iterables in a list when merging scalar sources', () {
final Iterable<String> iterable = Iterable.generate(1, (i) => 'it-$i');

final result = Utils.merge(iterable, 'tail');

expect(result, isA<List>());
final listResult = result as List;
expect(listResult.first, same(iterable));
expect(listResult.last, equals('tail'));
});

test('promotes iterable targets to index maps before merging maps', () {
final result = Utils.merge(
[const Undefined(), 'keep'],
{'extra': 1},
) as Map<String, dynamic>;

expect(result, equals({'1': 'keep', 'extra': 1}));
});

test('wraps scalar targets into heterogeneous lists when merging maps', () {
final result = Utils.merge(
'seed',
{'extra': 1},
) as List;

expect(result.first, equals('seed'));
expect(result.last, equals({'extra': 1}));
});
});

group('Utils.encode surrogate handling', () {
const int segmentLimit = 1024;

String buildBoundaryString() {
final high = String.fromCharCode(0xD83D);
final low = String.fromCharCode(0xDE00);
return '${'a' * (segmentLimit - 1)}$high${low}tail';
}

test('avoids splitting surrogate pairs across segments', () {
final encoded = Utils.encode(buildBoundaryString());
expect(encoded.startsWith('a' * (segmentLimit - 1)), isTrue);
expect(encoded, contains('%F0%9F%98%80'));
expect(encoded.endsWith('tail'), isTrue);
});

test('encodes high-and-low surrogate pair to four-byte UTF-8', () {
final emoji = String.fromCharCodes([0xD83D, 0xDE01]);
expect(Utils.encode(emoji), equals('%F0%9F%98%81'));
});

test('encodes lone high surrogate as three-byte sequence', () {
final loneHigh = String.fromCharCode(0xD83D);
expect(Utils.encode(loneHigh), equals('%ED%A0%BD'));
});

test('encodes lone low surrogate as three-byte sequence', () {
final loneLow = String.fromCharCode(0xDC00);
expect(Utils.encode(loneLow), equals('%ED%B0%80'));
});
});

group('Utils helpers', () {
test('isNonNullishPrimitive treats Uri based on skipNulls flag', () {
final emptyUri = Uri.parse('');
expect(Utils.isNonNullishPrimitive(emptyUri), isTrue);
expect(Utils.isNonNullishPrimitive(emptyUri, true), isFalse);
final populated = Uri.parse('https://example.com');
expect(Utils.isNonNullishPrimitive(populated, true), isTrue);
});

test('interpretNumericEntities handles astral plane code points', () {
expect(Utils.interpretNumericEntities('&#128512;'), equals('😀'));
});

test('createIndexMap materializes non-List iterables', () {
final iterable = Iterable.generate(3, (i) => i * 2);
expect(
Utils.createIndexMap(iterable),
equals({'0': 0, '1': 2, '2': 4}),
);
});
});
}