From 4bd492fe0ecda05da11cd872b3ce3358b8f9f7d7 Mon Sep 17 00:00:00 2001 From: Naveed Jooma Date: Wed, 2 Aug 2023 16:07:17 -0400 Subject: [PATCH 1/9] Document sensor widget --- lib/widgets/resources/sensor.dart | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/lib/widgets/resources/sensor.dart b/lib/widgets/resources/sensor.dart index 0156268861..8ce07d405f 100644 --- a/lib/widgets/resources/sensor.dart +++ b/lib/widgets/resources/sensor.dart @@ -6,10 +6,22 @@ import 'package:viam_sdk/viam_sdk.dart'; import '../button.dart'; +/// A widget to display data from a [Sensor]. +/// +/// Displays the data in a simple data table, with options for +/// displaying the time the data were retrieved, +/// automatic refreshing, and for controlling the refresh. class ViamSensorWidget extends StatefulWidget { + /// The [Sensor] final Sensor sensor; + + /// How often to automatically refresh the data final Duration? refreshInterval; + + /// Whether to display the time the data were retrieved final bool showLastRefreshed; + + /// Whether to display controls for refreshing data final bool showRefreshControls; const ViamSensorWidget({ @@ -103,13 +115,14 @@ class _ViamSensorWidgetState extends State { Column(children: [ const SizedBox(height: 8), Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - ViamButton( - onPressed: playPause, - text: isPaused ? 'Play' : 'Pause', - icon: isPaused ? Icons.play_arrow : Icons.pause, - variant: ViamButtonVariant.iconOnly, - style: ViamButtonFillStyle.ghost, - ), + if (widget.refreshInterval != null) + ViamButton( + onPressed: playPause, + text: isPaused ? 'Play' : 'Pause', + icon: isPaused ? Icons.play_arrow : Icons.pause, + variant: ViamButtonVariant.iconOnly, + style: ViamButtonFillStyle.ghost, + ), const SizedBox(width: 8), ViamButton( onPressed: refresh, From bc0786ac6b12a55e3839490ee4102b88dbaa60f6 Mon Sep 17 00:00:00 2001 From: Naveed Jooma Date: Wed, 2 Aug 2023 16:09:17 -0400 Subject: [PATCH 2/9] Document board widget --- lib/widgets/resources/board.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/widgets/resources/board.dart b/lib/widgets/resources/board.dart index 4afc8c5b7f..1e73bd67c8 100644 --- a/lib/widgets/resources/board.dart +++ b/lib/widgets/resources/board.dart @@ -3,7 +3,12 @@ import 'package:viam_sdk/viam_sdk.dart'; import '../button.dart'; +/// A widget to control a [Board]. +/// +/// Displays the status of any [DigitalInterrupts] or [AnalogReaders] in a data table. +/// Provides the ability to set specific GPIO pins to high/low states. class ViamBoardWidget extends StatefulWidget { + /// The [Board] final Board board; const ViamBoardWidget({ From 4e12935dc53ab24176400027ff0b1f7d3516e7ce Mon Sep 17 00:00:00 2001 From: Naveed Jooma Date: Wed, 2 Aug 2023 16:14:23 -0400 Subject: [PATCH 3/9] Document base widget --- lib/widgets/resources/base.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/widgets/resources/base.dart b/lib/widgets/resources/base.dart index f72594c1ad..18fefbf779 100644 --- a/lib/widgets/resources/base.dart +++ b/lib/widgets/resources/base.dart @@ -4,9 +4,18 @@ import 'package:viam_sdk/viam_sdk.dart'; import '../camera_stream.dart'; import '../joystick.dart'; +/// A widget to control a [Base]. +/// +/// This widget provides a joystick for moving a [Base], +/// along with displaying any camera streams that might be available on the robot. class ViamBaseScreen extends StatefulWidget { + /// The [Base] final Base base; + + /// Any [Camera]s that should be streamed final Iterable cameras; + + /// The current [RobotClient] final RobotClient robotClient; const ViamBaseScreen({ From 8b367917d57e44c23e5d0222e7213d54a9999fd9 Mon Sep 17 00:00:00 2001 From: Naveed Jooma Date: Wed, 2 Aug 2023 16:23:33 -0400 Subject: [PATCH 4/9] Document button widget --- lib/widgets/button.dart | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/lib/widgets/button.dart b/lib/widgets/button.dart index 17fd6d9c92..884e68d0bc 100644 --- a/lib/widgets/button.dart +++ b/lib/widgets/button.dart @@ -18,6 +18,7 @@ enum ViamButtonRole { /// A button to indicate a dangerous operation, will result in an red color scheme danger; + /// The background color of the button, based on role. Takes dark mode into consideration. Color get backgroundColor { final brightness = SchedulerBinding.instance.platformDispatcher.platformBrightness; final isDarkMode = brightness == Brightness.dark; @@ -35,6 +36,7 @@ enum ViamButtonRole { } } + /// The foreground color of the button, based on role. Takes dark mode into consideration. Color get foregroundColor { final brightness = SchedulerBinding.instance.platformDispatcher.platformBrightness; final isDarkMode = brightness == Brightness.dark; @@ -52,6 +54,7 @@ enum ViamButtonRole { } } + /// The material color of the button, based on role. MaterialColor get materialColor { switch (this) { case ViamButtonRole.primary: @@ -66,17 +69,21 @@ enum ViamButtonRole { return Colors.amber; } } - - ButtonStyle get style => - ButtonStyle(backgroundColor: MaterialStatePropertyAll(backgroundColor), foregroundColor: MaterialStatePropertyAll(foregroundColor)); } +/// The fill style of the button. enum ViamButtonFillStyle { + /// The button should be filled entirely filled, + + /// The button be outlined outline, + + /// The button's background should be transparent ghost; } +/// The size class of the button. enum ViamButtonSizeClass { xs, small, @@ -84,6 +91,7 @@ enum ViamButtonSizeClass { large, xl; + /// The font size of the button, based on size class double get fontSize { switch (this) { case xs: @@ -99,6 +107,7 @@ enum ViamButtonSizeClass { } } + /// The padding of the button, based on size class EdgeInsets get padding { switch (this) { case xs: @@ -114,24 +123,45 @@ enum ViamButtonSizeClass { } } + /// The style of the button, based on size class ButtonStyle get style { return ButtonStyle(textStyle: MaterialStatePropertyAll(TextStyle(fontSize: fontSize)), padding: MaterialStatePropertyAll(padding)); } } +/// The variant of the button enum ViamButtonVariant { + /// The button should only show the icon iconOnly, + + /// The icon should be ahead of the text iconLeading, + + /// The icon should be after the text iconTrailing; } +/// A button that has Viam specific styling class ViamButton extends StatelessWidget { + /// The text of the button final String text; + + /// The action that should be performed when the button is pressed final VoidCallback onPressed; + + /// The icon to display final IconData? icon; + + /// The role of the button final ViamButtonRole role; + + /// The fill style of the button final ViamButtonFillStyle style; + + /// The button variant final ViamButtonVariant variant; + + /// The size class of the button final ViamButtonSizeClass size; const ViamButton( @@ -153,7 +183,7 @@ class ViamButton extends StatelessWidget { fgColor = role.foregroundColor; } return mainStyle.copyWith( - backgroundColor: const MaterialStatePropertyAll(Color.fromARGB(0, 0, 0, 0)), + backgroundColor: const MaterialStatePropertyAll(Colors.transparent), foregroundColor: MaterialStatePropertyAll(fgColor), ); } From aa32e0db6d0afbd9d50efc90f1c0ef43b75ba796 Mon Sep 17 00:00:00 2001 From: Naveed Jooma Date: Wed, 2 Aug 2023 16:27:27 -0400 Subject: [PATCH 5/9] Document stream and joystick --- lib/widgets/camera_stream.dart | 4 ++++ lib/widgets/joystick.dart | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/widgets/camera_stream.dart b/lib/widgets/camera_stream.dart index 4e2b6ed768..518b3ee482 100644 --- a/lib/widgets/camera_stream.dart +++ b/lib/widgets/camera_stream.dart @@ -5,8 +5,12 @@ import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:logger/logger.dart'; import 'package:viam_sdk/viam_sdk.dart'; +/// A widget to display a WebRTC stream from a [Camera]. class ViamCameraStreamView extends StatefulWidget { + /// The [Camera] final Camera camera; + + /// The [StreamClient] for the specific [Camera] final StreamClient streamClient; const ViamCameraStreamView({super.key, required this.camera, required this.streamClient}); diff --git a/lib/widgets/joystick.dart b/lib/widgets/joystick.dart index 7d2f368011..61ef478ff0 100644 --- a/lib/widgets/joystick.dart +++ b/lib/widgets/joystick.dart @@ -2,12 +2,14 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_joystick/flutter_joystick.dart'; import 'package:viam_sdk/viam_sdk.dart'; +/// A [Joystick] to control a specific [Base] class ViamBaseJoystick extends StatefulWidget { const ViamBaseJoystick({ super.key, required this.base, }); + /// The [Base] final Base base; @override @@ -25,7 +27,7 @@ class _ViamBaseJoystickState extends State { Text('Y: ${y.round()}% Z: ${z.round()}%'), const SizedBox(height: 16), Joystick( - listener: (callSetPower), + listener: callSetPower, ) ], ); From 5d0865aa4354792a6e5424b5a1f7c68de64cf4e1 Mon Sep 17 00:00:00 2001 From: Naveed Jooma Date: Wed, 2 Aug 2023 17:19:13 -0400 Subject: [PATCH 6/9] Sensor widget tests --- test/test_utils.dart | 21 +++++++ test/widget_tests/sensor_widget_test.dart | 77 +++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 test/widget_tests/sensor_widget_test.dart diff --git a/test/test_utils.dart b/test/test_utils.dart index 57f9d3dc89..4d5540236e 100644 --- a/test/test_utils.dart +++ b/test/test_utils.dart @@ -1 +1,22 @@ +import 'package:flutter/material.dart'; + int generateTestingPortFromName(String name) => 50000 + (name.hashCode % 10000); + +class TestableWidget extends StatelessWidget { + const TestableWidget({ + super.key, + required this.child, + }); + + final Widget child; + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + home: Scaffold( + body: child, + ), + ); + } +} diff --git a/test/widget_tests/sensor_widget_test.dart b/test/widget_tests/sensor_widget_test.dart new file mode 100644 index 0000000000..4cd3ba123d --- /dev/null +++ b/test/widget_tests/sensor_widget_test.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:viam_sdk/widgets.dart'; + +import '../test_utils.dart'; +import '../unit_test/components/sensor_test.dart'; + +void main() { + group('ViamSensorWidget', () { + testWidgets('displays data', (tester) async { + final widget = TestableWidget(child: ViamSensorWidget(sensor: FakeSensor('sensor'))); + await tester.pumpWidget(widget); + + final dataTable = find.byType(DataTable); + + expect(dataTable, findsOneWidget); + }); + + testWidgets('shows refresh controls', (tester) async { + final widget = TestableWidget(child: ViamSensorWidget(sensor: FakeSensor('sensor'), showRefreshControls: true)); + await tester.pumpWidget(widget); + + final playButton = find.widgetWithIcon(ViamButton, Icons.play_arrow); + final pauseButton = find.widgetWithIcon(ViamButton, Icons.pause); + final refreshButton = find.widgetWithIcon(ViamButton, Icons.refresh); + + expect(playButton, findsNothing); + expect(pauseButton, findsOneWidget); + expect(refreshButton, findsOneWidget); + + await tester.press(pauseButton); + await tester.pump(); + + expect(playButton, findsNothing); + expect(pauseButton, findsOneWidget); + }); + + testWidgets('hides refresh controls', (tester) async { + final widget = TestableWidget(child: ViamSensorWidget(sensor: FakeSensor('sensor'), showRefreshControls: false)); + await tester.pumpWidget(widget); + + final playButton = find.widgetWithIcon(ViamButton, Icons.play_arrow); + final pauseButton = find.widgetWithIcon(ViamButton, Icons.pause); + final refreshButton = find.widgetWithIcon(ViamButton, Icons.refresh); + + expect(playButton, findsNothing); + expect(pauseButton, findsNothing); + expect(refreshButton, findsNothing); + }); + + testWidgets('shows last refresh time', (tester) async { + final widget = TestableWidget(child: ViamSensorWidget(sensor: FakeSensor('sensor'), showLastRefreshed: true)); + await tester.pumpWidget(widget); + + final refreshButton = find.widgetWithIcon(ViamButton, Icons.refresh); + await tester.press(refreshButton); + await tester.pump(); + + final refreshTime = find.textContaining(RegExp(r'Updated at:.*')); + + expect(refreshTime, findsOneWidget); + }); + + testWidgets('hides last refresh time', (tester) async { + final widget = TestableWidget(child: ViamSensorWidget(sensor: FakeSensor('sensor'), showLastRefreshed: false)); + await tester.pumpWidget(widget); + + final refreshButton = find.widgetWithIcon(ViamButton, Icons.refresh); + await tester.press(refreshButton); + await tester.pump(); + + final refreshTime = find.textContaining(RegExp(r'Updated at:.*')); + + expect(refreshTime, findsNothing); + }); + }); +} From 2898b146a60e54b6e292c001ab052d96b1557699 Mon Sep 17 00:00:00 2001 From: Naveed Jooma Date: Wed, 2 Aug 2023 17:23:28 -0400 Subject: [PATCH 7/9] Additional test --- test/widget_tests/sensor_widget_test.dart | 53 ++++++++++++++--------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/test/widget_tests/sensor_widget_test.dart b/test/widget_tests/sensor_widget_test.dart index 4cd3ba123d..46034b9ee0 100644 --- a/test/widget_tests/sensor_widget_test.dart +++ b/test/widget_tests/sensor_widget_test.dart @@ -16,6 +16,32 @@ void main() { expect(dataTable, findsOneWidget); }); + testWidgets('shows last refresh time', (tester) async { + final widget = TestableWidget(child: ViamSensorWidget(sensor: FakeSensor('sensor'), showLastRefreshed: true)); + await tester.pumpWidget(widget); + + final refreshButton = find.widgetWithIcon(ViamButton, Icons.refresh); + await tester.press(refreshButton); + await tester.pump(); + + final refreshTime = find.textContaining(RegExp(r'Updated at:.*')); + + expect(refreshTime, findsOneWidget); + }); + + testWidgets('hides last refresh time', (tester) async { + final widget = TestableWidget(child: ViamSensorWidget(sensor: FakeSensor('sensor'), showLastRefreshed: false)); + await tester.pumpWidget(widget); + + final refreshButton = find.widgetWithIcon(ViamButton, Icons.refresh); + await tester.press(refreshButton); + await tester.pump(); + + final refreshTime = find.textContaining(RegExp(r'Updated at:.*')); + + expect(refreshTime, findsNothing); + }); + testWidgets('shows refresh controls', (tester) async { final widget = TestableWidget(child: ViamSensorWidget(sensor: FakeSensor('sensor'), showRefreshControls: true)); await tester.pumpWidget(widget); @@ -48,30 +74,15 @@ void main() { expect(refreshButton, findsNothing); }); - testWidgets('shows last refresh time', (tester) async { - final widget = TestableWidget(child: ViamSensorWidget(sensor: FakeSensor('sensor'), showLastRefreshed: true)); + testWidgets('hides play/pause when refresh interval is null', (tester) async { + final widget = TestableWidget(child: ViamSensorWidget(sensor: FakeSensor('sensor'), refreshInterval: null)); await tester.pumpWidget(widget); - final refreshButton = find.widgetWithIcon(ViamButton, Icons.refresh); - await tester.press(refreshButton); - await tester.pump(); - - final refreshTime = find.textContaining(RegExp(r'Updated at:.*')); - - expect(refreshTime, findsOneWidget); - }); - - testWidgets('hides last refresh time', (tester) async { - final widget = TestableWidget(child: ViamSensorWidget(sensor: FakeSensor('sensor'), showLastRefreshed: false)); - await tester.pumpWidget(widget); - - final refreshButton = find.widgetWithIcon(ViamButton, Icons.refresh); - await tester.press(refreshButton); - await tester.pump(); - - final refreshTime = find.textContaining(RegExp(r'Updated at:.*')); + final playButton = find.widgetWithIcon(ViamButton, Icons.play_arrow); + final pauseButton = find.widgetWithIcon(ViamButton, Icons.pause); - expect(refreshTime, findsNothing); + expect(playButton, findsNothing); + expect(pauseButton, findsNothing); }); }); } From 438beeb88b13405a2833112366713337732254c7 Mon Sep 17 00:00:00 2001 From: Naveed Jooma Date: Wed, 2 Aug 2023 19:03:48 -0400 Subject: [PATCH 8/9] Add additional test for manual refresh button --- test/widget_tests/sensor_widget_test.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/widget_tests/sensor_widget_test.dart b/test/widget_tests/sensor_widget_test.dart index 46034b9ee0..d796488086 100644 --- a/test/widget_tests/sensor_widget_test.dart +++ b/test/widget_tests/sensor_widget_test.dart @@ -80,9 +80,11 @@ void main() { final playButton = find.widgetWithIcon(ViamButton, Icons.play_arrow); final pauseButton = find.widgetWithIcon(ViamButton, Icons.pause); + final refreshButton = find.widgetWithIcon(ViamButton, Icons.refresh); expect(playButton, findsNothing); expect(pauseButton, findsNothing); + expect(refreshButton, findsOneWidget); }); }); } From 3d810e124fdc27f7dcc6659e36fd350c96e1b139 Mon Sep 17 00:00:00 2001 From: Naveed Jooma Date: Wed, 2 Aug 2023 19:32:00 -0400 Subject: [PATCH 9/9] Cleanup button, add tooltip for icon only buttons --- lib/widgets/button.dart | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/lib/widgets/button.dart b/lib/widgets/button.dart index 884e68d0bc..692bbac745 100644 --- a/lib/widgets/button.dart +++ b/lib/widgets/button.dart @@ -211,26 +211,23 @@ class ViamButton extends StatelessWidget { @override Widget build(BuildContext context) { Widget child; - if (icon != null) { - final prePadding = (variant == ViamButtonVariant.iconOnly) ? const Text(' ') : const SizedBox.shrink(); - final iconWidget = Row(children: [prePadding, Icon(icon!)]); - final label = (variant == ViamButtonVariant.iconOnly) - ? const SizedBox.shrink() - : Text( - text, - style: const TextStyle(fontWeight: FontWeight.bold), - ); - final first = (variant == ViamButtonVariant.iconTrailing) ? label : iconWidget; - final second = (variant == ViamButtonVariant.iconTrailing) ? iconWidget : label; - if (style == ViamButtonFillStyle.outline) { - child = OutlinedButton.icon(onPressed: onPressed, icon: first, label: second, style: _buttonStyle); - } else { - child = TextButton.icon(onPressed: onPressed, icon: first, label: second, style: _buttonStyle); - } - } else if (style == ViamButtonFillStyle.outline) { - child = OutlinedButton(onPressed: onPressed, style: _buttonStyle, child: Text(text)); + final iconWidget = (icon != null) ? Icon(icon, size: size.fontSize * 1.25) : const SizedBox.shrink(); + final labelWidget = (variant == ViamButtonVariant.iconOnly) + ? const SizedBox.shrink() + : Text( + text, + style: TextStyle(fontWeight: (icon != null) ? FontWeight.bold : FontWeight.normal), + ); + final first = (variant == ViamButtonVariant.iconTrailing) ? labelWidget : iconWidget; + final second = (variant == ViamButtonVariant.iconTrailing) ? iconWidget : labelWidget; + final widget = Row(mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [first, second]); + if (style == ViamButtonFillStyle.outline) { + child = OutlinedButton(onPressed: onPressed, style: _buttonStyle, child: widget); } else { - child = TextButton(onPressed: onPressed, style: _buttonStyle, child: Text(text)); + child = TextButton(onPressed: onPressed, style: _buttonStyle, child: widget); + } + if (variant == ViamButtonVariant.iconOnly) { + child = Tooltip(message: text, waitDuration: const Duration(seconds: 1), child: child); } return Theme(data: ThemeData(primarySwatch: role.materialColor), child: child); }