implemented Ticker page selection persistence and enhanced ProseMirror table display modes

This commit is contained in:
2026-07-10 19:15:52 +02:00
parent 0d01f6b631
commit 37608e59b3
10 changed files with 827 additions and 131 deletions
+185 -13
View File
@@ -19,9 +19,19 @@ List<TickerNavSection> _sections() => [
),
];
Widget _host() => MaterialApp(
Widget _host({
List<TickerNavSection>? sections,
String? initialSlug,
void Function(String? slug)? onSelectionChanged,
String? homePublishedAt,
VoidCallback? onRefreshHome,
}) => MaterialApp(
home: TickerScaffold(
sections: _sections(),
sections: sections ?? _sections(),
initialSlug: initialSlug,
onSelectionChanged: onSelectionChanged,
homePublishedAt: homePublishedAt,
onRefreshHome: onRefreshHome,
homeBuilder: (context, onLinkTap) => const Text('HOME'),
pageBuilder: (context, slug, onLinkTap) => Text('PAGE:$slug'),
),
@@ -30,29 +40,50 @@ Widget _host() => MaterialApp(
Finder _appBarText(String text) =>
find.descendant(of: find.byType(AppBar), matching: find.text(text));
// The "Aktuelles" home action is always present; it is greyed out (disabled)
// while home is the current surface, active while a page is open.
bool _homeEnabled(WidgetTester tester) =>
tester
.widget<IconButton>(
find.widgetWithIcon(IconButton, Icons.home_outlined),
)
.onPressed !=
null;
// The home surface stays mounted (IndexedStack) while a page is open, so
// "which content is shown" is the stack index, not widget presence.
int _shownIndex(WidgetTester tester) =>
tester.widget<IndexedStack>(find.byType(IndexedStack)).index!;
const String _menuTooltip = 'Open navigation menu';
const String _navTooltip = 'Seiten';
void main() {
group('narrow layout (< 900)', () {
testWidgets('uses a drawer reachable via the burger button', (tester) async {
testWidgets('opens the nav in a bottom sheet from the floating button', (
tester,
) async {
await tester.pumpWidget(_host());
await tester.pumpAndSettle();
// Nav is hidden behind the drawer, not shown as a sidebar.
// No drawer, and the nav is hidden until the sheet is opened.
expect(find.byType(Drawer), findsNothing);
expect(find.text('Über uns'), findsNothing);
expect(find.byTooltip(_menuTooltip), findsOneWidget);
expect(find.byTooltip(_navTooltip), findsOneWidget);
await tester.tap(find.byTooltip(_menuTooltip));
await tester.tap(find.byTooltip(_navTooltip));
await tester.pumpAndSettle();
expect(find.byType(Drawer), findsOneWidget);
expect(find.text('Über uns'), findsOneWidget);
// Sheet shows the section-grouped nav.
expect(find.text('Aktuelles'), findsOneWidget);
expect(find.text('Über uns'), findsOneWidget);
// Selecting a page closes the sheet and switches the content in place.
await tester.tap(find.text('Über uns'));
await tester.pumpAndSettle();
expect(find.text('Aktuelles'), findsNothing);
expect(_shownIndex(tester), 1);
expect(_appBarText('Über uns'), findsOneWidget);
});
});
@@ -70,7 +101,8 @@ void main() {
await pumpWide(tester);
expect(find.byType(Drawer), findsNothing);
expect(find.byTooltip(_menuTooltip), findsNothing);
// No bottom-sheet trigger on tablets — the sidebar is permanent.
expect(find.byTooltip(_navTooltip), findsNothing);
// Sidebar nav item is visible without any interaction.
expect(find.text('Über uns'), findsOneWidget);
});
@@ -83,7 +115,9 @@ void main() {
expect(_shownIndex(tester), 0);
expect(find.text('PAGE:about'), findsNothing);
expect(_appBarText('Ticker'), findsOneWidget);
expect(find.byIcon(Icons.home_outlined), findsNothing);
// Home action is shown but greyed out on the home surface.
expect(find.byIcon(Icons.home_outlined), findsOneWidget);
expect(_homeEnabled(tester), isFalse);
await tester.tap(find.text('Über uns'));
await tester.pumpAndSettle();
@@ -91,7 +125,7 @@ void main() {
expect(_shownIndex(tester), 1);
expect(find.text('PAGE:about'), findsOneWidget);
expect(_appBarText('Über uns'), findsOneWidget);
expect(find.byIcon(Icons.home_outlined), findsOneWidget);
expect(_homeEnabled(tester), isTrue);
await tester.tap(find.byIcon(Icons.home_outlined));
await tester.pumpAndSettle();
@@ -99,7 +133,145 @@ void main() {
expect(_shownIndex(tester), 0);
expect(find.text('PAGE:about'), findsNothing);
expect(_appBarText('Ticker'), findsOneWidget);
expect(find.byIcon(Icons.home_outlined), findsNothing);
expect(find.byIcon(Icons.home_outlined), findsOneWidget);
expect(_homeEnabled(tester), isFalse);
});
});
group('remembered selection', () {
Future<void> pumpWide(WidgetTester tester, Widget host) async {
tester.view.physicalSize = const Size(1200, 2000);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(host);
await tester.pumpAndSettle();
}
testWidgets('reopens the persisted page on mount', (tester) async {
await tester.pumpWidget(_host(initialSlug: 'about'));
await tester.pumpAndSettle();
expect(_shownIndex(tester), 1);
expect(find.text('PAGE:about'), findsOneWidget);
expect(_appBarText('Über uns'), findsOneWidget);
expect(_homeEnabled(tester), isTrue);
});
testWidgets('falls back to home and drops a slug that is gone', (
tester,
) async {
String? persisted = 'ghost';
await tester.pumpWidget(
_host(initialSlug: 'ghost', onSelectionChanged: (slug) => persisted = slug),
);
await tester.pumpAndSettle();
expect(_shownIndex(tester), 0);
expect(_appBarText('Ticker'), findsOneWidget);
expect(persisted, isNull);
});
testWidgets('does not restore a redirect slug', (tester) async {
await tester.pumpWidget(_host(initialSlug: 'web'));
await tester.pumpAndSettle();
expect(_shownIndex(tester), 0);
});
testWidgets('reports selection changes so the host can persist them', (
tester,
) async {
final changes = <String?>[];
await pumpWide(tester, _host(onSelectionChanged: changes.add));
await tester.tap(find.text('Über uns'));
await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.home_outlined));
await tester.pumpAndSettle();
expect(changes, ['about', null]);
});
testWidgets('drops the open page when a fresh nav no longer lists it', (
tester,
) async {
String? persisted;
await pumpWide(
tester,
_host(onSelectionChanged: (slug) => persisted = slug),
);
await tester.tap(find.text('Über uns'));
await tester.pumpAndSettle();
expect(_shownIndex(tester), 1);
await tester.pumpWidget(
_host(
sections: [
TickerNavSection(
title: 'Infos',
pages: [
TickerNavPage(
title: 'Anderes',
slug: 'other',
kind: TickerPageKind.content,
),
],
),
],
onSelectionChanged: (slug) => persisted = slug,
),
);
await tester.pumpAndSettle();
expect(_shownIndex(tester), 0);
expect(_appBarText('Ticker'), findsOneWidget);
expect(persisted, isNull);
});
});
group('home date button', () {
testWidgets('shows the publish date on home and refreshes on tap', (
tester,
) async {
var refreshed = 0;
await tester.pumpWidget(
_host(
homePublishedAt: '2026-02-17T14:30:00',
onRefreshHome: () => refreshed++,
),
);
await tester.pumpAndSettle();
expect(find.text('17.02.2026, 14:30'), findsOneWidget);
await tester.tap(find.text('17.02.2026, 14:30'));
await tester.pumpAndSettle();
expect(refreshed, 1);
});
testWidgets('hides the date button when there is no date', (tester) async {
await tester.pumpWidget(_host());
await tester.pumpAndSettle();
expect(find.byIcon(Icons.schedule), findsNothing);
});
testWidgets('hides the date button once a page is open', (tester) async {
tester.view.physicalSize = const Size(1200, 2000);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(_host(homePublishedAt: '2026-02-17T14:30:00'));
await tester.pumpAndSettle();
expect(find.byIcon(Icons.schedule), findsOneWidget);
await tester.tap(find.text('Über uns'));
await tester.pumpAndSettle();
expect(find.byIcon(Icons.schedule), findsNothing);
});
});
@@ -0,0 +1,95 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_layout_grid/flutter_layout_grid.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:marianum_mobile/widget/prosemirror/pm_document_view.dart';
import 'package:marianum_mobile/widget/prosemirror/pm_node.dart';
import 'package:marianum_mobile/widget/prosemirror/pm_table_view.dart';
PmNode _tableDoc() {
final raw = File(
'test/widget/prosemirror/fixtures/table.json',
).readAsStringSync();
return PmNode.fromJson(jsonDecode(raw) as Map<String, dynamic>);
}
Widget _host(PmNode doc) => MaterialApp(
home: Scaffold(
body: SingleChildScrollView(child: PmDocumentView(doc: doc)),
),
);
PmTableCell _textCell(String text) => PmTableCell(
children: [
PmParagraph(children: [PmText(text: text)]),
],
);
void main() {
setUp(() => pmTableMode.value = PmTableMode.scroll);
tearDown(() => pmTableMode.value = PmTableMode.scroll);
testWidgets('renders in every display mode without exception', (tester) async {
await tester.pumpWidget(_host(_tableDoc()));
for (final mode in PmTableMode.values) {
pmTableMode.value = mode;
await tester.pumpAndSettle();
expect(tester.takeException(), isNull, reason: 'mode $mode threw');
}
});
testWidgets('cells in a row share one height when wrapping', (tester) async {
// One row, two cells: a short one and one long enough to wrap at the
// constrained width. Both cell boxes must end up the same height.
final doc = PmTable(
children: [
PmTableRow(
children: [
_textCell('kurz'),
_textCell('ein deutlich laengerer Text der ganz sicher umbricht'),
],
),
],
);
pmTableMode.value = PmTableMode.wrap;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: SizedBox(width: 200, child: PmDocumentView(doc: doc)),
),
),
),
);
await tester.pumpAndSettle();
final boxes = find.descendant(
of: find.byType(LayoutGrid),
matching: find.byType(DecoratedBox),
);
expect(boxes, findsNWidgets(2));
final first = tester.getSize(boxes.at(0)).height;
final second = tester.getSize(boxes.at(1)).height;
expect(first, greaterThan(0));
expect(first, moreOrLessEquals(second, epsilon: 0.5));
});
testWidgets('tapping a toolbar button switches the shared mode', (
tester,
) async {
await tester.pumpWidget(_host(_tableDoc()));
expect(pmTableMode.value, PmTableMode.scroll);
await tester.tap(find.byTooltip('Spalten umbrechen').first);
await tester.pumpAndSettle();
expect(pmTableMode.value, PmTableMode.wrap);
await tester.tap(find.byTooltip('Verkleinern (alles auf einen Blick)').first);
await tester.pumpAndSettle();
expect(pmTableMode.value, PmTableMode.zoom);
});
}