mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-10 20:27:01 +01:00
Below 600dp the notes list is a compact tile grid (title + priority/status cues). Tile height is derived so whole rows exactly fill the viewport between the status and navigation bars; past a screenful it scrolls vertically (no paging). Body and dates moved into the long-press sheet. Wider windows keep the list. Not using design_system FitGrid: it is square-celled and pages horizontally. Pins: design_system v0.3.0 (newest tag); also bumped crdt_sync_dart 0.11.2, sync_settings_ui 0.4.2, file_picker 13.3.1, idb_shim 2.11.1, sqflite_common_ffi 2.4.5, web 1.1.2 because the freshness gate was already red on them at HEAD. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
167 lines
5.2 KiB
Dart
167 lines
5.2 KiB
Dart
/// Pixel 6a geometry of the notes grid: whole tiles only, nothing under the
|
|
/// status bar or the navigation bar, no overflow, scrolls only past a screen.
|
|
library;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:todo/data/note.dart';
|
|
import 'package:todo/ui/note_grid_tile.dart';
|
|
|
|
import 'notes_list_harness.dart';
|
|
|
|
const _dpr = 2.625;
|
|
const _navBar = 63 / _dpr;
|
|
const _statusBar = 132 / _dpr;
|
|
const _screenHeight = 2400 / _dpr;
|
|
|
|
/// 1080x2400 px at 2.625, with the real system-bar insets.
|
|
void _pixel6a(WidgetTester t, {double keyboardDp = 0}) {
|
|
const bars = FakeViewPadding(top: 132, bottom: 63);
|
|
t.view
|
|
..physicalSize = const Size(1080, 2400)
|
|
..devicePixelRatio = _dpr
|
|
..padding = bars
|
|
..viewPadding = bars
|
|
..viewInsets = FakeViewPadding(bottom: keyboardDp * _dpr);
|
|
addTearDown(t.view.reset);
|
|
}
|
|
|
|
List<Note> _notes(int n) => [
|
|
for (var i = 0; i < n; i++)
|
|
note(
|
|
'n$i',
|
|
'Note $i with a rather long title that wraps onto several lines',
|
|
priority: Priority.values[i % 3],
|
|
),
|
|
];
|
|
|
|
Rect _viewport(WidgetTester t) => t.getRect(find.byType(GridView));
|
|
|
|
/// Tiles whose rect touches the viewport.
|
|
List<Rect> _shown(WidgetTester t) {
|
|
final view = _viewport(t);
|
|
return [
|
|
for (final e in find.byType(NoteGridTile).evaluate())
|
|
t.getRect(find.byWidget(e.widget)),
|
|
].where((r) => r.overlaps(view)).toList();
|
|
}
|
|
|
|
double _maxScroll(WidgetTester t) => t
|
|
.state<ScrollableState>(
|
|
find.descendant(
|
|
of: find.byType(GridView),
|
|
matching: find.byType(Scrollable),
|
|
),
|
|
)
|
|
.position
|
|
.maxScrollExtent;
|
|
|
|
void _expectWholeTilesOnly(WidgetTester t) {
|
|
final view = _viewport(t);
|
|
final shown = _shown(t);
|
|
expect(shown, isNotEmpty);
|
|
for (final r in shown) {
|
|
expect(r.top, greaterThanOrEqualTo(view.top - 0.01), reason: '$r');
|
|
expect(r.bottom, lessThanOrEqualTo(view.bottom + 0.01), reason: '$r');
|
|
}
|
|
// The rows use up the whole viewport: no strip of dead space under them.
|
|
final lastBottom = shown.map((r) => r.bottom).reduce((a, b) => a > b ? a : b);
|
|
expect(lastBottom, closeTo(view.bottom, 0.01));
|
|
}
|
|
|
|
void main() {
|
|
testWidgets('Pixel 6a: whole tiles between the system bars', (t) async {
|
|
_pixel6a(t);
|
|
await pumpList(t, seed: _notes(300));
|
|
|
|
expect(t.takeException(), isNull);
|
|
final view = _viewport(t);
|
|
expect(view.top, greaterThanOrEqualTo(_statusBar + kToolbarHeight));
|
|
expect(view.bottom, closeTo(_screenHeight - _navBar, 0.01));
|
|
_expectWholeTilesOnly(t);
|
|
// Two columns, a real screenful, and tiles big enough to read.
|
|
final shown = _shown(t);
|
|
expect(shown.length.isEven, isTrue);
|
|
expect(shown.length, greaterThanOrEqualTo(10));
|
|
expect(shown.first.height, greaterThanOrEqualTo(88));
|
|
expect(shown.first.width, closeTo(shown[1].width, 0.01));
|
|
expect(shown[1].left, greaterThan(shown.first.right));
|
|
});
|
|
|
|
testWidgets('Pixel 6a: 300 notes scroll vertically to the last one', (
|
|
t,
|
|
) async {
|
|
_pixel6a(t);
|
|
await pumpList(t, seed: _notes(300));
|
|
|
|
expect(_maxScroll(t), greaterThan(0));
|
|
await t.drag(find.byType(GridView), const Offset(0, -500000));
|
|
await t.pump();
|
|
await t.pump(const Duration(seconds: 1));
|
|
|
|
expect(find.textContaining('Note 299 '), findsOneWidget);
|
|
expect(find.textContaining('Note 299 ').hitTestable(), findsOneWidget);
|
|
expect(_viewport(t).bottom, closeTo(_screenHeight - _navBar, 0.01));
|
|
});
|
|
|
|
testWidgets('Pixel 6a: exactly one screenful does not scroll', (t) async {
|
|
_pixel6a(t);
|
|
await pumpList(t, seed: _notes(300));
|
|
final perScreen = _shown(t).length;
|
|
|
|
await t.pumpWidget(const SizedBox());
|
|
await pumpList(t, seed: _notes(perScreen));
|
|
expect(_maxScroll(t), 0);
|
|
_expectWholeTilesOnly(t);
|
|
expect(_shown(t), hasLength(perScreen));
|
|
});
|
|
|
|
testWidgets('Pixel 6a: a few notes keep full-size tiles at the top', (
|
|
t,
|
|
) async {
|
|
_pixel6a(t);
|
|
await pumpList(t, seed: _notes(300));
|
|
final full = _shown(t).first;
|
|
|
|
await t.pumpWidget(const SizedBox());
|
|
await pumpList(t, seed: _notes(3));
|
|
expect(_maxScroll(t), 0);
|
|
expect(t.takeException(), isNull);
|
|
final first = t.getRect(find.byType(NoteGridTile).first);
|
|
expect(first.size, full.size);
|
|
expect(first.top, closeTo(_viewport(t).top, 0.01));
|
|
});
|
|
|
|
testWidgets('Pixel 6a: toolbar, search and a tile are all tappable', (
|
|
t,
|
|
) async {
|
|
_pixel6a(t);
|
|
await pumpList(t, seed: _notes(20));
|
|
|
|
expect(find.byIcon(Icons.sort).hitTestable(), findsOneWidget);
|
|
expect(find.byIcon(Icons.filter_list).hitTestable(), findsOneWidget);
|
|
expect(find.byType(TextField).hitTestable(), findsOneWidget);
|
|
expect(find.byType(NoteGridTile).hitTestable(), findsWidgets);
|
|
});
|
|
|
|
testWidgets('Pixel 6a with the keyboard open: still whole tiles', (t) async {
|
|
_pixel6a(t, keyboardDp: 300);
|
|
await pumpList(t, seed: _notes(300));
|
|
|
|
expect(t.takeException(), isNull);
|
|
_expectWholeTilesOnly(t);
|
|
});
|
|
|
|
testWidgets('Pixel 6a at 2x text scale: no overflow, tiles stay whole', (
|
|
t,
|
|
) async {
|
|
_pixel6a(t);
|
|
t.platformDispatcher.textScaleFactorTestValue = 2;
|
|
addTearDown(t.platformDispatcher.clearTextScaleFactorTestValue);
|
|
await pumpList(t, seed: _notes(300));
|
|
|
|
expect(t.takeException(), isNull);
|
|
_expectWholeTilesOnly(t);
|
|
});
|
|
}
|