Refresh the Flutter snapshot: UI-01 through UI-09, plus a fresh installable APK
Full UI redesign pass complete: persistent tab shell with an always-visible background map, Modern Professional Dark theme, monochrome dark map tiles, offline skeleton map, a shared GlassPanel/FloatingPill component kit, customizable HUD telemetry widgets, and the Map HUD / Plan & Route Planning / Rides History screen rebuilds. 374 tests passing, up from 316. The APK is a fresh release build (debug-signed, no release signing config exists yet). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Xki7YAcc2TiN2PRZJ2tXr
This commit is contained in:
152
rippr-flutter-src/test/glass_component_kit_test.dart
Normal file
152
rippr-flutter-src/test/glass_component_kit_test.dart
Normal file
@@ -0,0 +1,152 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:rippr/src/ui/components/floating_pill.dart';
|
||||
import 'package:rippr/src/ui/components/glass_panel.dart';
|
||||
import 'package:rippr/src/ui/components/pulsing_location_marker.dart';
|
||||
import 'package:rippr/src/ui/theme.dart';
|
||||
|
||||
/// UI-03: structure/colour assertions, not pixels -- no golden files, per V3-16's own
|
||||
/// precedent (see that ticket's Tests section for the reasoning).
|
||||
void main() {
|
||||
group('GlassPanel', () {
|
||||
testWidgets('renders a blurred, bordered, translucent container', (tester) async {
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: ripprTheme(),
|
||||
home: const Scaffold(body: GlassPanel(child: Text('content'))),
|
||||
));
|
||||
|
||||
expect(find.byType(BackdropFilter), findsOneWidget);
|
||||
expect(find.text('content'), findsOneWidget);
|
||||
|
||||
final backdrop = tester.widget<BackdropFilter>(find.byType(BackdropFilter));
|
||||
expect(backdrop.filter, isA<ImageFilter>());
|
||||
|
||||
final box = tester.widget<DecoratedBox>(find.byType(DecoratedBox).first);
|
||||
final decoration = box.decoration as BoxDecoration;
|
||||
expect(decoration.color, isNotNull);
|
||||
expect(decoration.color!.a, closeTo(GlassPanel.fillOpacity, 0.01),
|
||||
reason: 'the fill must be translucent, not a solid card');
|
||||
expect(decoration.border, isNotNull);
|
||||
});
|
||||
|
||||
Future<void> expectLegible(WidgetTester tester, ThemeData theme) async {
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: theme,
|
||||
home: const Scaffold(body: GlassPanel(child: Text('Distance'))),
|
||||
));
|
||||
final colors = theme.colorScheme;
|
||||
// The actually-resolved paint colour, not the (null, since the Text above sets
|
||||
// no style of its own) `Text.style` -- this is what the black-on-black bug the
|
||||
// rest of the theme tests guard against would have shown up as.
|
||||
final paragraph = tester.renderObject<RenderParagraph>(find.text('Distance'));
|
||||
final resolvedColor = paragraph.text.style?.color;
|
||||
expect(resolvedColor, isNotNull,
|
||||
reason: 'text inside a GlassPanel must not fall back to an undefined '
|
||||
'colour -- the same failure mode the ambient bodyColor fix in '
|
||||
'ripprTheme() exists to prevent');
|
||||
expect(contrastRatio(resolvedColor!, colors.surface), greaterThanOrEqualTo(4.5));
|
||||
}
|
||||
|
||||
// Two separate tests, not one test looping over both themes and re-pumping into
|
||||
// the same tester -- pumping a second MaterialApp with an identically-shaped tree
|
||||
// reuses the first pump's elements rather than rebuilding from the new theme,
|
||||
// which silently re-asserted the first theme's already-resolved colour twice.
|
||||
testWidgets('is legible against the dark ground in the pocketed theme', (tester) {
|
||||
return expectLegible(tester, ripprTheme());
|
||||
});
|
||||
|
||||
testWidgets('is legible against the dark ground in the mounted theme', (tester) {
|
||||
return expectLegible(tester, ripprMountedTheme());
|
||||
});
|
||||
});
|
||||
|
||||
group('FloatingPill', () {
|
||||
testWidgets('renders an arbitrary number of stat columns with dividers between '
|
||||
'them', (tester) async {
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: ripprTheme(),
|
||||
home: const Scaffold(
|
||||
body: FloatingPill(
|
||||
stats: [
|
||||
PillStat(label: 'Distance', value: '12.4', unit: 'km'),
|
||||
PillStat(label: 'Est. Time', value: '45', unit: 'm'),
|
||||
PillStat(label: 'Pins', value: '5'),
|
||||
PillStat(label: 'Elevation', value: '210', unit: 'm'),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
expect(find.text('DISTANCE'), findsOneWidget);
|
||||
expect(find.text('EST. TIME'), findsOneWidget);
|
||||
expect(find.text('PINS'), findsOneWidget);
|
||||
expect(find.text('ELEVATION'), findsOneWidget);
|
||||
expect(find.text('12.4'), findsOneWidget);
|
||||
expect(find.text('km'), findsOneWidget);
|
||||
expect(find.text('5'), findsOneWidget);
|
||||
|
||||
// n stats -> n-1 dividers, whatever n is -- not hardcoded to three.
|
||||
final dividers = tester
|
||||
.widgetList<Container>(find.byType(Container))
|
||||
.where((c) => c.constraints?.maxWidth == 1)
|
||||
.toList();
|
||||
expect(dividers.length, 3);
|
||||
});
|
||||
|
||||
testWidgets('renders correctly with a single stat and no dividers', (tester) async {
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: ripprTheme(),
|
||||
home: const Scaffold(
|
||||
body: FloatingPill(stats: [PillStat(label: 'Pins', value: '0')]),
|
||||
),
|
||||
));
|
||||
|
||||
expect(find.text('PINS'), findsOneWidget);
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('PulsingLocationMarker', () {
|
||||
testWidgets('animates continuously', (tester) async {
|
||||
await tester.pumpWidget(const MaterialApp(
|
||||
home: Scaffold(body: PulsingLocationMarker()),
|
||||
));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
// Still ticking after a full second -- a one-shot animation would have
|
||||
// completed and stopped producing new frames by now.
|
||||
expect(tester.hasRunningAnimations, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('disposes its AnimationController when removed from the tree',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(const MaterialApp(
|
||||
home: Scaffold(body: PulsingLocationMarker()),
|
||||
));
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
await tester.pumpWidget(const MaterialApp(home: Scaffold(body: SizedBox())));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// No lingering ticker/timer -- flutter_test's own binding fails the test at
|
||||
// tearDown if a ticker from the removed widget is still registered.
|
||||
expect(tester.hasRunningAnimations, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('falls back to a static dot when reduced motion is enabled',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(const MediaQuery(
|
||||
data: MediaQueryData(disableAnimations: true),
|
||||
child: MaterialApp(home: Scaffold(body: PulsingLocationMarker())),
|
||||
));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(tester.hasRunningAnimations, isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user