testdrive 0.1.0
testdrive: ^0.1.0 copied to clipboard
Flutter SDK for TestDeck. Captures HTTP, logs, navigation, and exceptions from debug builds.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:testdrive/testdrive.dart';
import 'screens/errors_demo.dart';
import 'screens/http_demo.dart';
import 'screens/logs_demo.dart';
import 'screens/navigation_demo.dart';
import 'screens/stress_demo.dart';
void main() {
// The SDK is debug-only — guard with kDebugMode so it never ships to prod.
if (kDebugMode) {
TestDrive.init(
appName: 'TestDrive Example',
// All of these are the defaults; shown here to document the surface.
host: '127.0.0.1',
port: 9876,
maxBodyBytes: 1024 * 1024,
captureBodies: true,
captureLogs: true,
captureErrors: true,
maxEventsPerSecond: 200,
);
}
runApp(const TestDriveExampleApp());
}
class TestDriveExampleApp extends StatelessWidget {
const TestDriveExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'TestDrive Example',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorSchemeSeed: const Color(0xFF7AA2F7),
useMaterial3: true,
brightness: Brightness.dark,
),
// Routing all navigation through TestDrive's observer feeds the desktop
// Navigation tab.
navigatorObservers: [TestDrive.navigator()],
home: const HomeScreen(),
);
}
}
/// A demo entry: a card on the home screen.
class _Demo {
const _Demo(this.title, this.subtitle, this.icon, this.builder);
final String title;
final String subtitle;
final IconData icon;
final WidgetBuilder builder;
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
static final List<_Demo> _demos = [
_Demo(
'HTTP capture',
'GET / POST / image / error / 1 MB truncation',
Icons.swap_vert,
(_) => const HttpDemoScreen(),
),
_Demo(
'Logs',
'levels · structured payload · developer.log',
Icons.subject,
(_) => const LogsDemoScreen(),
),
_Demo(
'Navigation',
'push / pop / replace · JSON + non-JSON args',
Icons.alt_route,
(_) => const NavigationDemoScreen(),
),
_Demo(
'Errors',
'Flutter framework · async Dart · caught + logged',
Icons.error_outline,
(_) => const ErrorsDemoScreen(),
),
_Demo(
'Stress / rate limit',
'flood the transport to see drops + buffering',
Icons.bolt,
(_) => const StressDemoScreen(),
),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('TestDrive Example'),
centerTitle: false,
),
body: Column(
children: [
const TestDriveStatusPanel(),
const Divider(height: 1),
Expanded(
child: ListView.separated(
padding: const EdgeInsets.all(12),
itemCount: _demos.length,
separatorBuilder: (_, __) => const SizedBox(height: 8),
itemBuilder: (context, i) {
final d = _demos[i];
return Card(
margin: EdgeInsets.zero,
child: ListTile(
leading: CircleAvatar(child: Icon(d.icon)),
title: Text(d.title),
subtitle: Text(d.subtitle),
trailing: const Icon(Icons.chevron_right),
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(
settings: RouteSettings(name: '/${d.title}'),
builder: d.builder,
),
),
),
);
},
),
),
],
),
);
}
}
/// Live view of [TestDrive.status] — connection state and transport counters,
/// refreshed once a second. Shows the SDK working even before the desktop
/// connects (events buffer; the counter ticks up).
class TestDriveStatusPanel extends StatefulWidget {
const TestDriveStatusPanel({super.key});
@override
State<TestDriveStatusPanel> createState() => _TestDriveStatusPanelState();
}
class _TestDriveStatusPanelState extends State<TestDriveStatusPanel> {
Timer? _timer;
@override
void initState() {
super.initState();
_timer = Timer.periodic(
const Duration(seconds: 1),
(_) => setState(() {}),
);
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final s = TestDrive.status;
final connected = s.connected;
final color = !s.initialized
? Colors.grey
: connected
? Colors.greenAccent
: Colors.amberAccent;
final label = !s.initialized
? 'not initialized'
: connected
? 'connected'
: 'buffering (desktop offline)';
return Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.circle, size: 12, color: color),
const SizedBox(width: 8),
Text(
'testdrive · $label',
style: const TextStyle(fontWeight: FontWeight.w600),
),
const Spacer(),
Text('${s.host}:${s.port}',
style: Theme.of(context).textTheme.bodySmall),
],
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_Chip('buffered', '${s.buffered}'),
_Chip('flushed', '${s.eventsFlushed}'),
_Chip('dropped', '${s.eventsDropped}'),
],
),
],
),
);
}
}
class _Chip extends StatelessWidget {
const _Chip(this.label, this.value);
final String label;
final String value;
@override
Widget build(BuildContext context) {
return Chip(
visualDensity: VisualDensity.compact,
label: Text('$label: $value'),
);
}
}
/// Shared layout for a demo screen: a scrollable column of action buttons.
class DemoScaffold extends StatelessWidget {
const DemoScaffold({
super.key,
required this.title,
required this.intro,
required this.children,
});
final String title;
final String intro;
final List<Widget> children;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Text(intro, style: Theme.of(context).textTheme.bodyMedium),
const SizedBox(height: 16),
...children,
],
),
);
}
}
/// A full-width labelled action button used across the demo screens.
class DemoButton extends StatelessWidget {
const DemoButton(this.label, this.onPressed, {super.key, this.icon});
final String label;
final IconData? icon;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: SizedBox(
width: double.infinity,
child: FilledButton.tonalIcon(
onPressed: onPressed,
icon: Icon(icon ?? Icons.play_arrow),
label: Align(
alignment: Alignment.centerLeft,
child: Text(label),
),
),
),
);
}
}
/// Helper to flash a short confirmation without dragging BuildContext across
/// an async gap — callers capture this before awaiting.
void toast(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), duration: const Duration(seconds: 1)),
);
}