fix(ui): harden settings focus and semantics
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/utils/initials_palette.dart';
|
||||
|
||||
void main() {
|
||||
group('initialOf', () {
|
||||
test('keeps fallback, trimming, and ordinary casing behavior', () {
|
||||
expect(initialOf(''), '?');
|
||||
expect(initialOf(' \t\n'), '?');
|
||||
expect(initialOf(' alice '), 'A');
|
||||
});
|
||||
|
||||
test('returns the complete first extended grapheme', () {
|
||||
expect(initialOf('🇯🇵 Japan'), '🇯🇵');
|
||||
expect(initialOf('👍🏽 Approved'), '👍🏽');
|
||||
expect(initialOf('👨👩👧👦 Family'), '👨👩👧👦');
|
||||
expect(initialOf('e\u0301clair'), 'E\u0301');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/media/media_item.dart';
|
||||
import 'package:plezy/utils/search_relevance.dart';
|
||||
|
||||
import '../test_helpers/media_items.dart';
|
||||
|
||||
void main() {
|
||||
group('normalizeSearchText', () {
|
||||
test('normalizes canonical, compatibility, separator, and typography variants', () {
|
||||
expect(normalizeSearchText('Amélie'), normalizeSearchText('Ame\u0301lie'));
|
||||
expect(normalizeSearchText('Amelie'), normalizeSearchText('Amelie'));
|
||||
expect(normalizeSearchText('Spider\u00a0Man'), normalizeSearchText('Spider Man'));
|
||||
|
||||
final expected = normalizeSearchText('Spider Man');
|
||||
for (final value in ['Spider-Man', 'Spider‑Man', 'Spider–Man', 'Spider—Man']) {
|
||||
expect(normalizeSearchText(value), expected, reason: value);
|
||||
}
|
||||
});
|
||||
|
||||
test('preserves accents and script-significant marks', () {
|
||||
expect(normalizeSearchText('Cafe'), isNot(normalizeSearchText('Café')));
|
||||
expect(normalizeSearchText('Café'), normalizeSearchText('Cafe\u0301'));
|
||||
expect(normalizeSearchText('क'), isNot(normalizeSearchText('कि')));
|
||||
expect(normalizeSearchText('कि'), contains('ि'));
|
||||
});
|
||||
});
|
||||
|
||||
group('media search ranking', () {
|
||||
test('does not give an English-only leading article bonus', () {
|
||||
final items = [
|
||||
testMediaItem(id: 'the', title: 'The Boys'),
|
||||
testMediaItem(id: 'les', title: 'Les Boys'),
|
||||
testMediaItem(id: 'los', title: 'Los Boys'),
|
||||
];
|
||||
|
||||
expect(_ids(rankMediaSearchResults(items, 'Boys')), ['the', 'les', 'los']);
|
||||
expect(_ids(rankMediaSearchResults(items.reversed.toList(), 'Boys')), ['los', 'les', 'the']);
|
||||
});
|
||||
|
||||
test('keeps exact titles above longer prefixes', () {
|
||||
final items = [
|
||||
testMediaItem(id: 'longer', title: 'The Boys in the Boat'),
|
||||
testMediaItem(id: 'exact', title: 'The Boys'),
|
||||
];
|
||||
|
||||
expect(_ids(rankMediaSearchResults(items, 'The Boys')), ['exact', 'longer']);
|
||||
});
|
||||
|
||||
test('preserves nullable-field weighting', () {
|
||||
final items = [
|
||||
testMediaItem(id: 'parent', parentTitle: 'Target'),
|
||||
testMediaItem(id: 'original', originalTitle: 'Target'),
|
||||
];
|
||||
|
||||
expect(_ids(rankMediaSearchResults(items, 'Target')), ['original', 'parent']);
|
||||
});
|
||||
});
|
||||
|
||||
group('rankMediaSearchResults', () {
|
||||
test('ranks equivalent query forms identically and preserves normalized ties', () {
|
||||
final items = [
|
||||
testMediaItem(id: 'first', title: 'Spider-Man'),
|
||||
testMediaItem(id: 'second', title: 'Spider‑Man'),
|
||||
testMediaItem(id: 'third', title: 'Spider Man Returns'),
|
||||
testMediaItem(id: 'accented', title: 'Amélie'),
|
||||
testMediaItem(id: 'decomposed', title: 'Ame\u0301lie'),
|
||||
];
|
||||
|
||||
final asciiOrder = _ids(rankMediaSearchResults(items, 'Spider Man'));
|
||||
final compatibilityOrder = _ids(rankMediaSearchResults(items, 'Spider Man'));
|
||||
final typographicOrder = _ids(rankMediaSearchResults(items, 'Spider—Man'));
|
||||
|
||||
expect(compatibilityOrder, asciiOrder);
|
||||
expect(typographicOrder, asciiOrder);
|
||||
expect(asciiOrder.take(2), ['first', 'second']);
|
||||
expect(_ids(rankMediaSearchResults(items, 'Spider Man', limit: 1)), ['first']);
|
||||
expect(_ids(rankMediaSearchResults(items, 'Amélie')).take(2), ['accented', 'decomposed']);
|
||||
});
|
||||
|
||||
test('bounded selection matches the unbounded full ordering for a large input', () {
|
||||
final items = <MediaItem>[
|
||||
for (var i = 0; i < 1000; i++)
|
||||
testMediaItem(
|
||||
id: 'item-$i',
|
||||
title: switch (i) {
|
||||
999 => 'Target',
|
||||
_ when i % 137 == 0 => 'Target result $i',
|
||||
_ when i % 41 == 0 => 'A Target result $i',
|
||||
_ => 'Candidate $i',
|
||||
},
|
||||
),
|
||||
];
|
||||
final expected = _fullyRankedIds(items, 'Target').take(100).toList();
|
||||
|
||||
expect(_ids(rankMediaSearchResults(items, 'Target', limit: 100)), expected);
|
||||
expect(expected.first, 'item-999');
|
||||
});
|
||||
|
||||
test('retains the earliest items when an equal-score run crosses the cap', () {
|
||||
final items = [for (var i = 0; i < 150; i++) testMediaItem(id: 'tie-$i', title: 'Same title')];
|
||||
|
||||
expect(_ids(rankMediaSearchResults(items, 'Same title', limit: 100)), [for (var i = 0; i < 100; i++) 'tie-$i']);
|
||||
});
|
||||
|
||||
test('preserves limit boundaries, full ordering, and empty-query input order', () {
|
||||
final items = [
|
||||
testMediaItem(id: 'prefix', title: 'Target Extended'),
|
||||
testMediaItem(id: 'exact', title: 'Target'),
|
||||
testMediaItem(id: 'contains', title: 'A Target Story'),
|
||||
];
|
||||
final expected = _fullyRankedIds(items, 'Target');
|
||||
|
||||
expect(rankMediaSearchResults(items, 'Target', limit: 0), isEmpty);
|
||||
expect(_ids(rankMediaSearchResults(items, 'Target', limit: 1)), ['exact']);
|
||||
expect(_ids(rankMediaSearchResults(items, 'Target', limit: items.length)), expected);
|
||||
expect(_ids(rankMediaSearchResults(items, 'Target', limit: items.length + 5)), expected);
|
||||
expect(_ids(rankMediaSearchResults(items, 'Target')), expected);
|
||||
expect(_ids(rankMediaSearchResults(items, '— ‑ !!!', limit: 2)), ['prefix', 'exact']);
|
||||
});
|
||||
|
||||
test('keeps the negative-limit failure contract', () {
|
||||
final items = [testMediaItem(title: 'Target')];
|
||||
|
||||
expect(() => rankMediaSearchResults(items, 'Target', limit: -1), throwsRangeError);
|
||||
expect(() => rankMediaSearchResults(items, '!!!', limit: -1), throwsRangeError);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
List<String> _ids(Iterable<MediaItem> items) => [for (final item in items) item.id];
|
||||
|
||||
List<String> _fullyRankedIds(List<MediaItem> items, String query) => _ids(rankMediaSearchResults(items, query));
|
||||
Reference in New Issue
Block a user