fix(learn): Settings no longer jumps while scrolled up slowly

Settings built its cards as they scrolled into view and let go of those
far above. Scrolled back up, a card built anew asked again for what it
shows and grew once answered: Japanese on this device stays hidden until
the instance says what it offers devices, and Jellyfin's form fills in
the server the instance suggests and looks it up. Whatever was on the
screen was pushed down by as much, and near the top the list put right
where it had thought the first cards were.

Settings is short, so it is built whole now (`SingleChildScrollView`):
nothing is let go of on the way, text typed into Jellyfin's form stays
when scrolled away, and the scrollbar knows the list's length.

The new test scrolls a phone's Settings up from the bottom, 20 pixels at
a time, and holds every card to its place in the list; before, a card
moved 64 pixels. The citation test now scrolls to the dropdown it taps,
not to the guide's link below it, which only left the dropdown on the
screen while the cards beneath weren't built yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-25 13:19:53 +01:00
commit c38dc69018
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -1,4 +1,5 @@
import 'dart:io';
import 'dart:math' as math;
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/core/citation/model/citation.dart';
@ -7,6 +8,7 @@ import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
import 'package:halkyon_learn_flutter/src/core/version/app_version.dart';
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/device_dictionary.dart';
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:material_ui/material_ui.dart';
import '../../../support/fake_device_dictionaries.dart';
@ -54,6 +56,55 @@ void main() {
expect(find.text('Version 1.2.3 (build 45)'), findsOneWidget);
});
testWidgets('scrolled up slowly from the bottom, the cards move only as '
'far as the screen scrolls, though some grow once the instance '
'answers', (tester) async {
tester.view.physicalSize = const Size(400, 800);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await pumpApp(
tester,
testDependencies(
// Shown only once the instance said what it offers devices.
deviceDictionaries: deviceDictionariesIn(
FakeDeviceDictionaryGateway(offered: offeredJapanese()),
),
),
location: RoutePaths.settings,
);
await tester.pumpAndSettle();
final position = tester
.state<ScrollableState>(find.byType(Scrollable).first)
.position;
position.jumpTo(position.maxScrollExtent);
await tester.pumpAndSettle();
// Where each card built stands in the list: its place on the screen
// plus how far the list is scrolled, which stays as long as nothing
// above it changes its height.
Map<Element, double> places() => {
for (final card in find.byType(SectionCard).evaluate())
card:
tester.getTopLeft(find.byElementPredicate((e) => e == card)).dy +
position.pixels,
};
while (position.pixels > 0) {
final before = places();
position.jumpTo(math.max(0, position.pixels - 20));
await tester.pumpAndSettle();
final after = places();
for (final MapEntry(key: card, value: place) in before.entries) {
if (after[card] case final now?) {
expect(
now,
moreOrLessEquals(place),
reason: 'a card moved at ${position.pixels}',
);
}
}
}
});
group('Japanese on this device', () {
/// Opens Settings, tall enough to show every card at once.
Future<void> openSettings(
@ -161,7 +212,7 @@ void main() {
location: RoutePaths.settings,
);
await tester.scrollUntilVisible(
find.text('Official guide'),
find.text('APA (7th edition)'),
200,
scrollable: find.byType(Scrollable).first,
);