fix(ui): harden settings focus and semantics

This commit is contained in:
edde746
2026-07-24 03:46:50 +02:00
parent f8bfecf57d
commit b41fb4fe75
159 changed files with 12054 additions and 2050 deletions
+19
View File
@@ -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');
});
});
}
+132
View File
@@ -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', 'SpiderMan', 'SpiderMan', '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: 'SpiderMan'),
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));