Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RSDK-4265] Sensor widget tests #81

Merged
merged 9 commits into from
Aug 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 50 additions & 23 deletions lib/widgets/button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -52,6 +54,7 @@ enum ViamButtonRole {
}
}

/// The material color of the button, based on role.
MaterialColor get materialColor {
switch (this) {
case ViamButtonRole.primary:
Expand All @@ -66,24 +69,29 @@ 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,
medium,
large,
xl;

/// The font size of the button, based on size class
double get fontSize {
switch (this) {
case xs:
Expand All @@ -99,6 +107,7 @@ enum ViamButtonSizeClass {
}
}

/// The padding of the button, based on size class
EdgeInsets get padding {
switch (this) {
case xs:
Expand All @@ -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(
Expand All @@ -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),
);
}
Expand Down Expand Up @@ -181,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);
}
Expand Down
4 changes: 4 additions & 0 deletions lib/widgets/camera_stream.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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});
Expand Down
4 changes: 3 additions & 1 deletion lib/widgets/joystick.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,7 +27,7 @@ class _ViamBaseJoystickState extends State<ViamBaseJoystick> {
Text('Y: ${y.round()}% Z: ${z.round()}%'),
const SizedBox(height: 16),
Joystick(
listener: (callSetPower),
listener: callSetPower,
)
],
);
Expand Down
9 changes: 9 additions & 0 deletions lib/widgets/resources/base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<Camera> cameras;

/// The current [RobotClient]
final RobotClient robotClient;

const ViamBaseScreen({
Expand Down
5 changes: 5 additions & 0 deletions lib/widgets/resources/board.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
27 changes: 20 additions & 7 deletions lib/widgets/resources/sensor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -103,13 +115,14 @@ class _ViamSensorWidgetState extends State<ViamSensorWidget> {
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,
Expand Down
21 changes: 21 additions & 0 deletions test/test_utils.dart
Original file line number Diff line number Diff line change
@@ -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,
),
);
}
}
90 changes: 90 additions & 0 deletions test/widget_tests/sensor_widget_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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 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);

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);
});
Copy link
Member

@jckras jckras Aug 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(q) how come you aren't calling tester.pump() in this test like you are in the other ones?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tester.pump only needs to be called if the state changes in order to reevaluate the UI with the new state see docs


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 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);
});
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(q) maybe obvious but does it also hide the refresh button? should we still test for that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope! When refreshInterval is null, only the play/pause button is hidden. The manual refresh button needs to be available so that users can manually refresh

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But you are correct that we should have a test to make sure the refresh button is visible so I added it :)

}