From d202f7ed1e228b70c97daaf08808a4bc7c3d75ef Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 17 May 2024 17:52:37 +0200 Subject: [PATCH 001/119] Explore painting dot --- .fvmrc | 3 ++ lib/src/act/act.dart | 20 +++++++++---- lib/src/screenshot/screenshot.dart | 46 +++++++++++++++++++++++------- test/spot/exists_once_test.dart | 1 + 4 files changed, 55 insertions(+), 15 deletions(-) create mode 100644 .fvmrc diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 00000000..2549cd21 --- /dev/null +++ b/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "3.10.0" +} \ No newline at end of file diff --git a/lib/src/act/act.dart b/lib/src/act/act.dart index 0b657b18..3734db56 100644 --- a/lib/src/act/act.dart +++ b/lib/src/act/act.dart @@ -71,10 +71,11 @@ class Act { /// Triggers a tap event on a given widget. Future tap(WidgetSelector selector) async { - // Check if widget is in the widget tree. Throws if not. + // Check if the widget is in the widget tree. Throws if not. final snapshot = selector.snapshot()..existsOnce(); - - return TestAsyncUtils.guard(() async { + Offset? hitPosition; + Element? hitElement; + final guard = TestAsyncUtils.guard(() async { return _alwaysPropagateDevicePointerEvents(() async { // Find the associated RenderObject to get the position of the element on the screen final element = snapshot.discoveredElement!; @@ -87,7 +88,7 @@ class Act { } if (renderObject is! RenderBox) { throw TestFailure( - "Widget '${selector.toStringBreadcrumb()}' is associated to $renderObject which " + "Widget '${selector.toStringBreadcrumb()}' is associated with $renderObject which " "is not a RenderObject in the 2D Cartesian coordinate system " "(implements RenderBox).\n" "Spot does not know how to hit test such a widget.", @@ -108,7 +109,7 @@ class Act { final binding = TestWidgetsFlutterBinding.instance; - // Finally, tap the widget by sending a down and up event. + // Perform the tap by sending a down and up event on the original widget final downEvent = PointerDownEvent(position: centerPosition); binding.handlePointerEvent(downEvent); @@ -116,8 +117,17 @@ class Act { binding.handlePointerEvent(upEvent); await binding.pump(); + hitPosition = centerPosition; + hitElement = element; }); }); + await guard; + if (hitPosition != null && hitElement != null) { + await takeScreenshot( + element: hitElement, + hitPosition: hitPosition, + ); + } } // Validates that the widget is at least partially visible in the viewport. diff --git a/lib/src/screenshot/screenshot.dart b/lib/src/screenshot/screenshot.dart index 763250ca..37d428fa 100644 --- a/lib/src/screenshot/screenshot.dart +++ b/lib/src/screenshot/screenshot.dart @@ -8,6 +8,7 @@ import 'package:dartx/dartx_io.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:meta/meta.dart'; import 'package:nanoid2/nanoid2.dart'; import 'package:spot/spot.dart'; import 'package:spot/src/screenshot/screenshot.dart' as self @@ -48,8 +49,9 @@ Future takeScreenshot({ WidgetSnapshot? snapshot, WidgetSelector? selector, String? name, + Offset? hitPosition, }) async { - final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance; + final binding = TestWidgetsFlutterBinding.instance; final Frame? frame = _caller(); // Element that is currently active in the widget tree, to take a screenshot of @@ -65,7 +67,7 @@ Future takeScreenshot({ final elements = snapshot.discovered; if (elements.length > 1) { throw StateError( - 'Screenshots can only be taken of a single elements. ' + 'Screenshots can only be taken of a single element. ' 'The snapshot of ${snapshot.selector} contains ${elements.length} elements. ' 'Use a more specific selector to narrow down the scope of the screenshot.', ); @@ -73,13 +75,13 @@ Future takeScreenshot({ final element = elements.first.element; if (!element.mounted) { throw StateError( - 'Can not take a screenshot of snapshot $snapshot, because it is not mounted anymore. ' + 'Cannot take a screenshot of snapshot $snapshot, because it is not mounted anymore. ' 'Only Elements that are currently mounted can be screenshotted.', ); } if (snapshot.discoveredWidget != element.widget) { throw StateError( - 'Can not take a screenshot of snapshot $snapshot, because the Element has been updated since the snapshot was taken. ' + 'Cannot take a screenshot of snapshot $snapshot, because the Element has been updated since the snapshot was taken. ' 'This happens when the widget tree is rebuilt.', ); } @@ -89,7 +91,7 @@ Future takeScreenshot({ if (element != null) { if (!element.mounted) { throw StateError( - 'Can not take a screenshot of Element $element, because it is not mounted anymore. ' + 'Cannot take a screenshot of Element $element, because it is not mounted anymore. ' 'Only Elements that are currently mounted can be screenshotted.', ); } @@ -97,22 +99,27 @@ Future takeScreenshot({ } // fallback to screenshotting the entire app - // Deprecated, but as of today there is no multi window support for widget tests + // Deprecated, but as of today there is no multi-window support for widget tests // ignore: deprecated_member_use return binding.renderViewElement!; }(); late final Uint8List bytes; + late final ui.Image image; await binding.runAsync(() async { - final image = await _captureImage(liveElement); + image = await _captureImage(liveElement); final byteData = await image.toByteData(format: ui.ImageByteFormat.png); if (byteData == null) { - return 'Could not take screenshot'; + throw 'Could not take screenshot'; } bytes = byteData.buffer.asUint8List(); - image.dispose(); }); + // Overlay the red dot on the screenshot if centerPosition is available + final modifiedImage = hitPosition != null + ? await _overlayRedDotOnImage(image, hitPosition) + : bytes; + final spotTempDir = Directory.systemTemp.directory('spot'); if (!spotTempDir.existsSync()) { spotTempDir.createSync(); @@ -143,7 +150,7 @@ Future takeScreenshot({ return '$n-$uniqueId.png'; }(); final file = spotTempDir.file(screenshotFileName); - file.writeAsBytesSync(bytes); + file.writeAsBytesSync(modifiedImage); // ignore: avoid_print core.print( 'Screenshot file://${file.path}\n' @@ -152,6 +159,25 @@ Future takeScreenshot({ return Screenshot(file: file, initiator: frame); } +Future _overlayRedDotOnImage( + ui.Image image, Offset centerPosition) async { + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + + // Draw the original image + canvas.drawImage(image, Offset.zero, Paint()); + + // Draw the red dot + final paint = Paint()..color = const Color(0xFFFF0000); // Red color + canvas.drawCircle(centerPosition, 5.0, paint); // Radius of the red dot is 5.0 + + final picture = recorder.endRecording(); + final finalImage = await picture.toImage(image.width, image.height); + + final byteData = await finalImage.toByteData(format: ui.ImageByteFormat.png); + return byteData!.buffer.asUint8List(); +} + /// Provides the ability to create screenshots of a [WidgetSelector] extension SelectorScreenshotExtension on WidgetSelector { /// Takes as screenshot of the widget that can be found by this selector. diff --git a/test/spot/exists_once_test.dart b/test/spot/exists_once_test.dart index e24b1108..0140f223 100644 --- a/test/spot/exists_once_test.dart +++ b/test/spot/exists_once_test.dart @@ -46,6 +46,7 @@ void main() { ); spot().spot
().existsOnce(); + await takeScreenshot(); spot
( parents: [spot()], children: [spot()], From 776d5b4e48cb41c8d5923c25fed7f7822553084c Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 17 May 2024 18:27:54 +0200 Subject: [PATCH 002/119] Make future complete --- lib/src/act/act.dart | 25 ++++++++++--------------- lib/src/screenshot/screenshot.dart | 16 +++++++++++----- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/lib/src/act/act.dart b/lib/src/act/act.dart index 3734db56..0af92573 100644 --- a/lib/src/act/act.dart +++ b/lib/src/act/act.dart @@ -71,11 +71,10 @@ class Act { /// Triggers a tap event on a given widget. Future tap(WidgetSelector selector) async { - // Check if the widget is in the widget tree. Throws if not. + // Check if widget is in the widget tree. Throws if not. final snapshot = selector.snapshot()..existsOnce(); - Offset? hitPosition; - Element? hitElement; - final guard = TestAsyncUtils.guard(() async { + + return TestAsyncUtils.guard(() async { return _alwaysPropagateDevicePointerEvents(() async { // Find the associated RenderObject to get the position of the element on the screen final element = snapshot.discoveredElement!; @@ -88,7 +87,7 @@ class Act { } if (renderObject is! RenderBox) { throw TestFailure( - "Widget '${selector.toStringBreadcrumb()}' is associated with $renderObject which " + "Widget '${selector.toStringBreadcrumb()}' is associated to $renderObject which " "is not a RenderObject in the 2D Cartesian coordinate system " "(implements RenderBox).\n" "Spot does not know how to hit test such a widget.", @@ -109,7 +108,7 @@ class Act { final binding = TestWidgetsFlutterBinding.instance; - // Perform the tap by sending a down and up event on the original widget + // Finally, tap the widget by sending a down and up event. final downEvent = PointerDownEvent(position: centerPosition); binding.handlePointerEvent(downEvent); @@ -117,17 +116,13 @@ class Act { binding.handlePointerEvent(upEvent); await binding.pump(); - hitPosition = centerPosition; - hitElement = element; + + await takeScreenshot( + element: element, + hitPosition: centerPosition, + ); }); }); - await guard; - if (hitPosition != null && hitElement != null) { - await takeScreenshot( - element: hitElement, - hitPosition: hitPosition, - ); - } } // Validates that the widget is at least partially visible in the viewport. diff --git a/lib/src/screenshot/screenshot.dart b/lib/src/screenshot/screenshot.dart index 37d428fa..1d1a8e6f 100644 --- a/lib/src/screenshot/screenshot.dart +++ b/lib/src/screenshot/screenshot.dart @@ -8,7 +8,6 @@ import 'package:dartx/dartx_io.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:meta/meta.dart'; import 'package:nanoid2/nanoid2.dart'; import 'package:spot/spot.dart'; import 'package:spot/src/screenshot/screenshot.dart' as self @@ -115,10 +114,15 @@ Future takeScreenshot({ bytes = byteData.buffer.asUint8List(); }); + Future bytesWithHitMarker() async { + return binding.runAsync(() async { + return _overlayRedDotOnImage(image, hitPosition!); + }); + } + // Overlay the red dot on the screenshot if centerPosition is available - final modifiedImage = hitPosition != null - ? await _overlayRedDotOnImage(image, hitPosition) - : bytes; + final Uint8List modifiedImage = + (hitPosition != null ? await bytesWithHitMarker() : null) ?? bytes; final spotTempDir = Directory.systemTemp.directory('spot'); if (!spotTempDir.existsSync()) { @@ -160,7 +164,9 @@ Future takeScreenshot({ } Future _overlayRedDotOnImage( - ui.Image image, Offset centerPosition) async { + ui.Image image, + Offset centerPosition, +) async { final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); From bbd197ada17b0fe8984fe44281a394ce0417b79f Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 17 May 2024 18:38:51 +0200 Subject: [PATCH 003/119] Use blue crosshair --- lib/src/screenshot/screenshot.dart | 32 +++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/lib/src/screenshot/screenshot.dart b/lib/src/screenshot/screenshot.dart index 1d1a8e6f..19432bb2 100644 --- a/lib/src/screenshot/screenshot.dart +++ b/lib/src/screenshot/screenshot.dart @@ -116,7 +116,7 @@ Future takeScreenshot({ Future bytesWithHitMarker() async { return binding.runAsync(() async { - return _overlayRedDotOnImage(image, hitPosition!); + return _overlayCrosshairOnImage(image, hitPosition!); }); } @@ -163,7 +163,7 @@ Future takeScreenshot({ return Screenshot(file: file, initiator: frame); } -Future _overlayRedDotOnImage( +Future _overlayCrosshairOnImage( ui.Image image, Offset centerPosition, ) async { @@ -173,9 +173,31 @@ Future _overlayRedDotOnImage( // Draw the original image canvas.drawImage(image, Offset.zero, Paint()); - // Draw the red dot - final paint = Paint()..color = const Color(0xFFFF0000); // Red color - canvas.drawCircle(centerPosition, 5.0, paint); // Radius of the red dot is 5.0 + // Define the paint for the crosshair + final paint = Paint() + ..color = const Color(0xFF00FFFF) // Cyan color + ..strokeWidth = 2.0; + + // Draw vertical line + canvas.drawLine( + Offset(centerPosition.dx, centerPosition.dy - 10), + Offset(centerPosition.dx, centerPosition.dy + 10), + paint, + ); + + // Draw horizontal line + canvas.drawLine( + Offset(centerPosition.dx - 10, centerPosition.dy), + Offset(centerPosition.dx + 10, centerPosition.dy), + paint, + ); + + // Draw the circle intersecting the lines at half length + final circlePaint = Paint() + ..color = const Color(0xFF00FFFF) // Cyan color + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0; + canvas.drawCircle(centerPosition, 5.0, circlePaint); final picture = recorder.endRecording(); final finalImage = await picture.toImage(image.width, image.height); From b3d01ca6ffcc43d9d79db328cab52edcf554fceb Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Tue, 21 May 2024 15:37:34 +0200 Subject: [PATCH 004/119] Add abstract class ScreenshotAnnotator --- lib/src/screenshot/screenshot_annotator.dart | 56 ++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 lib/src/screenshot/screenshot_annotator.dart diff --git a/lib/src/screenshot/screenshot_annotator.dart b/lib/src/screenshot/screenshot_annotator.dart new file mode 100644 index 00000000..7ccbf76d --- /dev/null +++ b/lib/src/screenshot/screenshot_annotator.dart @@ -0,0 +1,56 @@ +import 'dart:core'; +import 'dart:ui' as ui; +import 'package:flutter/rendering.dart'; + +/// An annotator that can draw on a screenshot image. +abstract class ScreenshotAnnotator { + /// Annotate the [image] with additional graphics. + Future annotate(ui.Image image); +} + +/// Annotator that draws a crosshair at a given position. +class CrosshairAnnotator implements ScreenshotAnnotator { + /// The center position of the crosshair. + final Offset centerPosition; + + /// Creates a [CrosshairAnnotator] with a [centerPosition]. + const CrosshairAnnotator({required this.centerPosition}); + + @override + Future annotate(ui.Image image) async { + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + + // Draw the original image + canvas.drawImage(image, Offset.zero, Paint()); + + // Define the paint for the cross hair + final paint = Paint() + ..color = const Color(0xFF00FFFF) // Cyan color + ..strokeWidth = 2.0; + + // Draw vertical line + canvas.drawLine( + Offset(centerPosition.dx, centerPosition.dy - 20), + Offset(centerPosition.dx, centerPosition.dy + 20), + paint, + ); + + // Draw horizontal line + canvas.drawLine( + Offset(centerPosition.dx - 20, centerPosition.dy), + Offset(centerPosition.dx + 20, centerPosition.dy), + paint, + ); + + // Draw the circle intersecting the lines at half length + final circlePaint = Paint() + ..color = const Color(0xFF00FFFF) // Cyan color + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0; + canvas.drawCircle(centerPosition, 10.0, circlePaint); + + final picture = recorder.endRecording(); + return picture.toImage(image.width, image.height); + } +} From dd90edc7ba004911d2df576c755b0bb7a6bf2ce7 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Tue, 21 May 2024 15:39:54 +0200 Subject: [PATCH 005/119] Refactor screenshot class and add takeScreenshotWithCrosshair --- lib/src/screenshot/screenshot.dart | 117 ++++++++++++++--------------- 1 file changed, 56 insertions(+), 61 deletions(-) diff --git a/lib/src/screenshot/screenshot.dart b/lib/src/screenshot/screenshot.dart index 19432bb2..39384e2d 100644 --- a/lib/src/screenshot/screenshot.dart +++ b/lib/src/screenshot/screenshot.dart @@ -12,6 +12,7 @@ import 'package:nanoid2/nanoid2.dart'; import 'package:spot/spot.dart'; import 'package:spot/src/screenshot/screenshot.dart' as self show takeScreenshot; +import 'package:spot/src/screenshot/screenshot_annotator.dart'; import 'package:stack_trace/stack_trace.dart'; export 'package:stack_trace/stack_trace.dart' show Frame; @@ -48,7 +49,50 @@ Future takeScreenshot({ WidgetSnapshot? snapshot, WidgetSelector? selector, String? name, - Offset? hitPosition, +}) async { + return _createScreenshot( + element: element, + snapshot: snapshot, + selector: selector, + name: name, + ); +} + +/// Takes a screenshot of the entire screen or a single widget and annotates it +/// with a tap marker in form of a crosshair at the specified [crosshairPosition]. +/// +/// Provide an [element], [snapshot], or [selector] to specify what to capture. +/// - [element]: The specific element to capture. +/// - [snapshot]: A snapshot of the widget to capture. +/// - [selector]: A selector to determine the widget to capture. +/// - [name]: The name of the screenshot file. +/// - [tapPosition]: The position where the tap marker should be placed. +/// +/// The screenshot will have a crosshair painted at the [hitPosition]. +/// +/// Returns a [Screenshot] object containing the file and initiator frame. +Future takeScreenshotWithCrosshair({ + Element? element, + WidgetSnapshot? snapshot, + WidgetSelector? selector, + String? name, + required Offset crosshairPosition, +}) async { + return _createScreenshot( + element: element, + snapshot: snapshot, + selector: selector, + name: name, + annotator: CrosshairAnnotator(centerPosition: crosshairPosition), + ); +} + +Future _createScreenshot({ + Element? element, + WidgetSnapshot? snapshot, + WidgetSelector? selector, + String? name, + ScreenshotAnnotator? annotator, }) async { final binding = TestWidgetsFlutterBinding.instance; final Frame? frame = _caller(); @@ -103,31 +147,24 @@ Future takeScreenshot({ return binding.renderViewElement!; }(); - late final Uint8List bytes; - late final ui.Image image; + late final Uint8List image; await binding.runAsync(() async { - image = await _captureImage(liveElement); - final byteData = await image.toByteData(format: ui.ImageByteFormat.png); + final plainImage = await _captureImage(liveElement); + final ui.Image imageToCapture = + await annotator?.annotate(plainImage) ?? plainImage; + final byteData = + await imageToCapture.toByteData(format: ui.ImageByteFormat.png); if (byteData == null) { throw 'Could not take screenshot'; } - bytes = byteData.buffer.asUint8List(); + image = byteData.buffer.asUint8List(); }); - Future bytesWithHitMarker() async { - return binding.runAsync(() async { - return _overlayCrosshairOnImage(image, hitPosition!); - }); - } - - // Overlay the red dot on the screenshot if centerPosition is available - final Uint8List modifiedImage = - (hitPosition != null ? await bytesWithHitMarker() : null) ?? bytes; - final spotTempDir = Directory.systemTemp.directory('spot'); if (!spotTempDir.existsSync()) { spotTempDir.createSync(); } + String callerFileName() { final file = frame?.uri.pathSegments.last.replaceFirst('.dart', ''); final line = frame?.line; @@ -148,62 +185,20 @@ Future takeScreenshot({ } else { n = callerFileName(); } - // always append a unique id to avoid name collisions final uniqueId = nanoid(length: 5); return '$n-$uniqueId.png'; }(); + final file = spotTempDir.file(screenshotFileName); - file.writeAsBytesSync(modifiedImage); + file.writeAsBytesSync(image); // ignore: avoid_print core.print( 'Screenshot file://${file.path}\n' ' taken at ${frame?.member} ${frame?.uri}:${frame?.line}:${frame?.column}', ); - return Screenshot(file: file, initiator: frame); -} - -Future _overlayCrosshairOnImage( - ui.Image image, - Offset centerPosition, -) async { - final recorder = ui.PictureRecorder(); - final canvas = Canvas(recorder); - - // Draw the original image - canvas.drawImage(image, Offset.zero, Paint()); - - // Define the paint for the crosshair - final paint = Paint() - ..color = const Color(0xFF00FFFF) // Cyan color - ..strokeWidth = 2.0; - - // Draw vertical line - canvas.drawLine( - Offset(centerPosition.dx, centerPosition.dy - 10), - Offset(centerPosition.dx, centerPosition.dy + 10), - paint, - ); - // Draw horizontal line - canvas.drawLine( - Offset(centerPosition.dx - 10, centerPosition.dy), - Offset(centerPosition.dx + 10, centerPosition.dy), - paint, - ); - - // Draw the circle intersecting the lines at half length - final circlePaint = Paint() - ..color = const Color(0xFF00FFFF) // Cyan color - ..style = PaintingStyle.stroke - ..strokeWidth = 2.0; - canvas.drawCircle(centerPosition, 5.0, circlePaint); - - final picture = recorder.endRecording(); - final finalImage = await picture.toImage(image.width, image.height); - - final byteData = await finalImage.toByteData(format: ui.ImageByteFormat.png); - return byteData!.buffer.asUint8List(); + return Screenshot(file: file, initiator: frame); } /// Provides the ability to create screenshots of a [WidgetSelector] From 068eb2aab6efcf42f8a196b0dfbddd1fb5681433 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Tue, 21 May 2024 16:27:14 +0200 Subject: [PATCH 006/119] Fix typos --- test/spot/screenshot_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/spot/screenshot_test.dart b/test/spot/screenshot_test.dart index 23b35c02..9f9b1fd1 100644 --- a/test/spot/screenshot_test.dart +++ b/test/spot/screenshot_test.dart @@ -92,7 +92,7 @@ void main() { await expectLater( takeScreenshot(snapshot: containerSnapshot), throwsErrorContaining([ - 'Can not take a screenshot of snapshot', + 'Cannot take a screenshot of snapshot', 'not mounted anymore', 'Only Elements that are currently mounted can be screenshotted.', ]), @@ -141,7 +141,7 @@ void main() { await expectLater( takeScreenshot(element: containerElement), throwsErrorContaining([ - 'Can not take a screenshot of Element', + 'Cannot take a screenshot of Element', 'not mounted anymore', 'Only Elements that are currently mounted can be screenshotted.', ]), From b6f0b8eb8dabda7c292638cd032a66053f80e20d Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Tue, 21 May 2024 16:27:38 +0200 Subject: [PATCH 007/119] Rm fvmrc --- .fvmrc | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .fvmrc diff --git a/.fvmrc b/.fvmrc deleted file mode 100644 index 2549cd21..00000000 --- a/.fvmrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "flutter": "3.10.0" -} \ No newline at end of file From 9d0bd238a390ff9063fff812d2fd79428660299d Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Tue, 21 May 2024 16:28:17 +0200 Subject: [PATCH 008/119] Add crosshair shot option on tap --- lib/src/act/act.dart | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/src/act/act.dart b/lib/src/act/act.dart index 0af92573..85b2843c 100644 --- a/lib/src/act/act.dart +++ b/lib/src/act/act.dart @@ -4,6 +4,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; +import 'package:spot/src/screenshot/screenshot.dart'; import 'package:spot/src/spot/snapshot.dart'; /// Top level entry point to interact with widgets on the screen. @@ -70,7 +71,12 @@ class Act { } /// Triggers a tap event on a given widget. - Future tap(WidgetSelector selector) async { + /// If [showTapPosition] is true, a crosshair is drawn on the screenshot at + /// the position of the tap. + Future tap( + WidgetSelector selector, { + bool? showTapPosition, + }) async { // Check if widget is in the widget tree. Throws if not. final snapshot = selector.snapshot()..existsOnce(); @@ -117,10 +123,12 @@ class Act { await binding.pump(); - await takeScreenshot( - element: element, - hitPosition: centerPosition, - ); + if (showTapPosition == true) { + await takeScreenshotWithCrosshair( + element: element, + crosshairPosition: centerPosition, + ); + } }); }); } From db866442314d6ed3023192cea1748cbe04302973 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Tue, 21 May 2024 16:36:55 +0200 Subject: [PATCH 009/119] Improve naming of image bytes --- lib/src/screenshot/screenshot.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/src/screenshot/screenshot.dart b/lib/src/screenshot/screenshot.dart index 39384e2d..daf68862 100644 --- a/lib/src/screenshot/screenshot.dart +++ b/lib/src/screenshot/screenshot.dart @@ -147,17 +147,17 @@ Future _createScreenshot({ return binding.renderViewElement!; }(); - late final Uint8List image; + late final Uint8List imageBytes; await binding.runAsync(() async { - final plainImage = await _captureImage(liveElement); + final baseImage = await _captureImage(liveElement); final ui.Image imageToCapture = - await annotator?.annotate(plainImage) ?? plainImage; + await annotator?.annotate(baseImage) ?? baseImage; final byteData = await imageToCapture.toByteData(format: ui.ImageByteFormat.png); if (byteData == null) { throw 'Could not take screenshot'; } - image = byteData.buffer.asUint8List(); + imageBytes = byteData.buffer.asUint8List(); }); final spotTempDir = Directory.systemTemp.directory('spot'); @@ -191,7 +191,7 @@ Future _createScreenshot({ }(); final file = spotTempDir.file(screenshotFileName); - file.writeAsBytesSync(image); + file.writeAsBytesSync(imageBytes); // ignore: avoid_print core.print( 'Screenshot file://${file.path}\n' From a9c58ee4489b09c0fdcbeb91ee3edd325f1a6123 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Tue, 21 May 2024 16:37:06 +0200 Subject: [PATCH 010/119] Rm takeScreenshot from test --- test/spot/exists_once_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/test/spot/exists_once_test.dart b/test/spot/exists_once_test.dart index 0140f223..e24b1108 100644 --- a/test/spot/exists_once_test.dart +++ b/test/spot/exists_once_test.dart @@ -46,7 +46,6 @@ void main() { ); spot().spot
().existsOnce(); - await takeScreenshot(); spot
( parents: [spot()], children: [spot()], From 1d11a492d7b0acbc5edf6e8c9d9df9a34b0a1434 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Tue, 21 May 2024 18:27:43 +0200 Subject: [PATCH 011/119] Add tests --- lib/src/screenshot/screenshot.dart | 23 +-- lib/src/screenshot/screenshot_annotator.dart | 6 + test/spot/screenshot_test.dart | 163 +++++++++++++++++++ 3 files changed, 181 insertions(+), 11 deletions(-) diff --git a/lib/src/screenshot/screenshot.dart b/lib/src/screenshot/screenshot.dart index daf68862..a4b30171 100644 --- a/lib/src/screenshot/screenshot.dart +++ b/lib/src/screenshot/screenshot.dart @@ -117,14 +117,16 @@ Future _createScreenshot({ } final element = elements.first.element; if (!element.mounted) { + final reason = annotator == null ? '' : ' with ${annotator.name}'; throw StateError( - 'Cannot take a screenshot of snapshot $snapshot, because it is not mounted anymore. ' + 'Cannot take a screenshot of snapshot$reason, because it is not mounted anymore. ' 'Only Elements that are currently mounted can be screenshotted.', ); } if (snapshot.discoveredWidget != element.widget) { + final reason = annotator == null ? '' : ' with ${annotator.name}'; throw StateError( - 'Cannot take a screenshot of snapshot $snapshot, because the Element has been updated since the snapshot was taken. ' + 'Cannot take a screenshot of snapshot$reason, because the Element has been updated since the snapshot was taken. ' 'This happens when the widget tree is rebuilt.', ); } @@ -133,8 +135,9 @@ Future _createScreenshot({ if (element != null) { if (!element.mounted) { + final reason = annotator == null ? '' : ' with ${annotator.name}'; throw StateError( - 'Cannot take a screenshot of Element $element, because it is not mounted anymore. ' + 'Cannot take a screenshot of Element$reason, because it is not mounted anymore. ' 'Only Elements that are currently mounted can be screenshotted.', ); } @@ -147,17 +150,17 @@ Future _createScreenshot({ return binding.renderViewElement!; }(); - late final Uint8List imageBytes; + late final Uint8List image; await binding.runAsync(() async { - final baseImage = await _captureImage(liveElement); + final plainImage = await _captureImage(liveElement); final ui.Image imageToCapture = - await annotator?.annotate(baseImage) ?? baseImage; + await annotator?.annotate(plainImage) ?? plainImage; final byteData = await imageToCapture.toByteData(format: ui.ImageByteFormat.png); if (byteData == null) { throw 'Could not take screenshot'; } - imageBytes = byteData.buffer.asUint8List(); + image = byteData.buffer.asUint8List(); }); final spotTempDir = Directory.systemTemp.directory('spot'); @@ -180,19 +183,17 @@ Future _createScreenshot({ final String screenshotFileName = () { final String n; if (name != null) { - // escape / n = Uri.encodeQueryComponent(name); } else { n = callerFileName(); } - // always append a unique id to avoid name collisions final uniqueId = nanoid(length: 5); return '$n-$uniqueId.png'; }(); final file = spotTempDir.file(screenshotFileName); - file.writeAsBytesSync(imageBytes); - // ignore: avoid_print + file.writeAsBytesSync(image); + core.print( 'Screenshot file://${file.path}\n' ' taken at ${frame?.member} ${frame?.uri}:${frame?.line}:${frame?.column}', diff --git a/lib/src/screenshot/screenshot_annotator.dart b/lib/src/screenshot/screenshot_annotator.dart index 7ccbf76d..76e0c4e3 100644 --- a/lib/src/screenshot/screenshot_annotator.dart +++ b/lib/src/screenshot/screenshot_annotator.dart @@ -6,6 +6,9 @@ import 'package:flutter/rendering.dart'; abstract class ScreenshotAnnotator { /// Annotate the [image] with additional graphics. Future annotate(ui.Image image); + + /// The name of the annotator. + String get name; } /// Annotator that draws a crosshair at a given position. @@ -13,6 +16,9 @@ class CrosshairAnnotator implements ScreenshotAnnotator { /// The center position of the crosshair. final Offset centerPosition; + @override + String get name => 'Crosshair Annotator'; + /// Creates a [CrosshairAnnotator] with a [centerPosition]. const CrosshairAnnotator({required this.centerPosition}); diff --git a/test/spot/screenshot_test.dart b/test/spot/screenshot_test.dart index 9f9b1fd1..714e9e02 100644 --- a/test/spot/screenshot_test.dart +++ b/test/spot/screenshot_test.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image/image.dart' as img; import 'package:spot/spot.dart'; +import 'package:spot/src/screenshot/screenshot.dart'; import '../util/assert_error.dart'; @@ -263,6 +264,168 @@ void main() { ), ); }); + + group('Annotage Screenshot test', () { + testWidgets('Take screenshot with tap marker of the entire app', + (tester) async { + tester.view.physicalSize = const Size(210, 210); + tester.view.devicePixelRatio = 1.0; + const red = Color(0xffff0000); + await tester.pumpWidget( + Center( + child: Container(height: 200, width: 200, color: red), + ), + ); + + final shot = await takeScreenshotWithCrosshair( + crosshairPosition: Offset(105, 105), + ); + expect(shot.file.existsSync(), isTrue); + + final cyanPixelCoverage = + await percentageOfPixelsWithColor(shot.file, Color(0xFF00FFFF)); + expect(cyanPixelCoverage, greaterThan(0.0)); + }); + + testWidgets('Take screenshot with tap marker from a selector', + (tester) async { + tester.view.physicalSize = const Size(1000, 1000); + tester.view.devicePixelRatio = 1.0; + const red = Color(0xffff0000); + await tester.pumpWidget( + Center( + child: RepaintBoundary( + child: Container(height: 200, width: 200, color: red), + ), + ), + ); + + final container = await takeScreenshotWithCrosshair( + selector: spot(), + crosshairPosition: Offset(100, 100), + ); + expect(container.file.existsSync(), isTrue); + + final cyanPixelCoverage = + await percentageOfPixelsWithColor(container.file, Color(0xFF00FFFF)); + expect(cyanPixelCoverage, greaterThan(0.0)); + }); + + testWidgets('Take screenshot with tap marker from a snapshot', + (tester) async { + tester.view.physicalSize = const Size(1000, 1000); + tester.view.devicePixelRatio = 1.0; + const red = Color(0xffff0000); + await tester.pumpWidget( + Center( + child: RepaintBoundary( + child: Container(height: 200, width: 200, color: red), + ), + ), + ); + final containerSnapshot = spot().snapshot(); + + final container = await takeScreenshotWithCrosshair( + snapshot: containerSnapshot, + crosshairPosition: Offset(100, 100), + ); + expect(container.file.existsSync(), isTrue); + + final cyanPixelCoverage = + await percentageOfPixelsWithColor(container.file, Color(0xFF00FFFF)); + expect(cyanPixelCoverage, greaterThan(0.0)); + }); + + testWidgets( + 'Take screenshot with tap marker from a snapshot throws when snapshot is outdated', + (tester) async { + tester.view.physicalSize = const Size(1000, 1000); + tester.view.devicePixelRatio = 1.0; + const red = Color(0xffff0000); + await tester.pumpWidget( + Center( + child: RepaintBoundary( + child: Container(height: 200, width: 200, color: red), + ), + ), + ); + final containerSnapshot = spot().snapshot(); + + // Remove element that is captured in the snapshot + await tester.pumpWidget(Container()); + expect(containerSnapshot.discoveredElement!.mounted, isFalse); + + await expectLater( + takeScreenshotWithCrosshair( + snapshot: containerSnapshot, + crosshairPosition: Offset(100, 100), + ), + throwsErrorContaining([ + 'Cannot take a screenshot of snapshot with Crosshair Annotator', + 'not mounted anymore', + 'Only Elements that are currently mounted can be screenshotted.', + ]), + ); + }, + ); + + testWidgets('Take screenshot with tap marker from an element', + (tester) async { + tester.view.physicalSize = const Size(1000, 1000); + tester.view.devicePixelRatio = 1.0; + const red = Color(0xffff0000); + await tester.pumpWidget( + Center( + child: RepaintBoundary( + child: Container(height: 200, width: 200, color: red), + ), + ), + ); + final containerElement = spot().snapshot().discoveredElement; + + final container = await takeScreenshotWithCrosshair( + element: containerElement, + crosshairPosition: Offset(100, 100), + ); + expect(container.file.existsSync(), isTrue); + + final cyanPixelCoverage = + await percentageOfPixelsWithColor(container.file, Color(0xFF00FFFF)); + expect(cyanPixelCoverage, greaterThan(0.0)); + }); + + testWidgets( + 'takeScreenshotWithCrosshair throws when element does not exist anymore', + (tester) async { + tester.view.physicalSize = const Size(1000, 1000); + tester.view.devicePixelRatio = 1.0; + const red = Color(0xffff0000); + await tester.pumpWidget( + Center( + child: RepaintBoundary( + child: Container(height: 200, width: 200, color: red), + ), + ), + ); + final containerElement = spot().snapshot().discoveredElement; + + // Remove containerElement + await tester.pumpWidget(Container()); + expect(containerElement!.mounted, isFalse); + + await expectLater( + takeScreenshotWithCrosshair( + element: containerElement, + crosshairPosition: Offset(100, 100), + ), + throwsErrorContaining([ + 'Cannot take a screenshot of Element with Crosshair Annotator', + 'not mounted anymore', + 'Only Elements that are currently mounted can be screenshotted.', + ]), + ); + }); + }); } /// Parses an png image file and reads the percentage of pixels of a given [color]. From c99c85dcab2478c449f0e78839b2bba4aa57a3cc Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 22 May 2024 02:31:07 +0200 Subject: [PATCH 012/119] Rename arg, add doc --- lib/src/act/act.dart | 2 +- lib/src/screenshot/screenshot.dart | 10 ++++++---- lib/src/screenshot/screenshot_annotator.dart | 9 ++------- test/spot/screenshot_test.dart | 14 ++++++-------- 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/lib/src/act/act.dart b/lib/src/act/act.dart index 85b2843c..bd0d0614 100644 --- a/lib/src/act/act.dart +++ b/lib/src/act/act.dart @@ -126,7 +126,7 @@ class Act { if (showTapPosition == true) { await takeScreenshotWithCrosshair( element: element, - crosshairPosition: centerPosition, + centerPosition: centerPosition, ); } }); diff --git a/lib/src/screenshot/screenshot.dart b/lib/src/screenshot/screenshot.dart index a4b30171..500c274e 100644 --- a/lib/src/screenshot/screenshot.dart +++ b/lib/src/screenshot/screenshot.dart @@ -59,7 +59,7 @@ Future takeScreenshot({ } /// Takes a screenshot of the entire screen or a single widget and annotates it -/// with a tap marker in form of a crosshair at the specified [crosshairPosition]. +/// with a tap marker in form of a crosshair at the specified [centerPosition]. /// /// Provide an [element], [snapshot], or [selector] to specify what to capture. /// - [element]: The specific element to capture. @@ -76,14 +76,14 @@ Future takeScreenshotWithCrosshair({ WidgetSnapshot? snapshot, WidgetSelector? selector, String? name, - required Offset crosshairPosition, + required Offset centerPosition, }) async { return _createScreenshot( element: element, snapshot: snapshot, selector: selector, name: name, - annotator: CrosshairAnnotator(centerPosition: crosshairPosition), + annotator: CrosshairAnnotator(centerPosition: centerPosition), ); } @@ -171,6 +171,7 @@ Future _createScreenshot({ String callerFileName() { final file = frame?.uri.pathSegments.last.replaceFirst('.dart', ''); final line = frame?.line; + // escape / if (file != null && line != null) { return '$file:$line'; } @@ -187,13 +188,14 @@ Future _createScreenshot({ } else { n = callerFileName(); } + // always append a unique id to avoid name collisions final uniqueId = nanoid(length: 5); return '$n-$uniqueId.png'; }(); final file = spotTempDir.file(screenshotFileName); file.writeAsBytesSync(image); - +// ignore: avoid_print core.print( 'Screenshot file://${file.path}\n' ' taken at ${frame?.member} ${frame?.uri}:${frame?.line}:${frame?.column}', diff --git a/lib/src/screenshot/screenshot_annotator.dart b/lib/src/screenshot/screenshot_annotator.dart index 76e0c4e3..d2241352 100644 --- a/lib/src/screenshot/screenshot_annotator.dart +++ b/lib/src/screenshot/screenshot_annotator.dart @@ -27,31 +27,26 @@ class CrosshairAnnotator implements ScreenshotAnnotator { final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); - // Draw the original image canvas.drawImage(image, Offset.zero, Paint()); - // Define the paint for the cross hair final paint = Paint() - ..color = const Color(0xFF00FFFF) // Cyan color + ..color = const Color(0xFF00FFFF) ..strokeWidth = 2.0; - // Draw vertical line canvas.drawLine( Offset(centerPosition.dx, centerPosition.dy - 20), Offset(centerPosition.dx, centerPosition.dy + 20), paint, ); - // Draw horizontal line canvas.drawLine( Offset(centerPosition.dx - 20, centerPosition.dy), Offset(centerPosition.dx + 20, centerPosition.dy), paint, ); - // Draw the circle intersecting the lines at half length final circlePaint = Paint() - ..color = const Color(0xFF00FFFF) // Cyan color + ..color = const Color(0xFF00FFFF) ..style = PaintingStyle.stroke ..strokeWidth = 2.0; canvas.drawCircle(centerPosition, 10.0, circlePaint); diff --git a/test/spot/screenshot_test.dart b/test/spot/screenshot_test.dart index 714e9e02..5806c1c5 100644 --- a/test/spot/screenshot_test.dart +++ b/test/spot/screenshot_test.dart @@ -8,8 +8,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image/image.dart' as img; import 'package:spot/spot.dart'; -import 'package:spot/src/screenshot/screenshot.dart'; - import '../util/assert_error.dart'; void main() { @@ -278,7 +276,7 @@ void main() { ); final shot = await takeScreenshotWithCrosshair( - crosshairPosition: Offset(105, 105), + centerPosition: Offset(105, 105), ); expect(shot.file.existsSync(), isTrue); @@ -302,7 +300,7 @@ void main() { final container = await takeScreenshotWithCrosshair( selector: spot(), - crosshairPosition: Offset(100, 100), + centerPosition: Offset(100, 100), ); expect(container.file.existsSync(), isTrue); @@ -327,7 +325,7 @@ void main() { final container = await takeScreenshotWithCrosshair( snapshot: containerSnapshot, - crosshairPosition: Offset(100, 100), + centerPosition: Offset(100, 100), ); expect(container.file.existsSync(), isTrue); @@ -358,7 +356,7 @@ void main() { await expectLater( takeScreenshotWithCrosshair( snapshot: containerSnapshot, - crosshairPosition: Offset(100, 100), + centerPosition: Offset(100, 100), ), throwsErrorContaining([ 'Cannot take a screenshot of snapshot with Crosshair Annotator', @@ -385,7 +383,7 @@ void main() { final container = await takeScreenshotWithCrosshair( element: containerElement, - crosshairPosition: Offset(100, 100), + centerPosition: Offset(100, 100), ); expect(container.file.existsSync(), isTrue); @@ -416,7 +414,7 @@ void main() { await expectLater( takeScreenshotWithCrosshair( element: containerElement, - crosshairPosition: Offset(100, 100), + centerPosition: Offset(100, 100), ), throwsErrorContaining([ 'Cannot take a screenshot of Element with Crosshair Annotator', From d6f03724c6c5438dde4d6174492738c9d6460778 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 22 May 2024 02:31:20 +0200 Subject: [PATCH 013/119] Export method --- lib/spot.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/spot.dart b/lib/spot.dart index ab46ea60..dca6eb93 100644 --- a/lib/spot.dart +++ b/lib/spot.dart @@ -23,7 +23,8 @@ export 'package:spot/src/screenshot/screenshot.dart' Screenshot, SelectorScreenshotExtension, SnapshotScreenshotExtension, - takeScreenshot; + takeScreenshot, + takeScreenshotWithCrosshair; export 'package:spot/src/spot/default_selectors.dart' show DefaultWidgetMatchers, DefaultWidgetSelectors; export 'package:spot/src/spot/diagnostic_props.dart' From 1c8b25416df676d8b46f659bba0cca3e94f87a8a Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 22 May 2024 11:44:42 +0200 Subject: [PATCH 014/119] Remove unused import --- lib/src/act/act.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/act/act.dart b/lib/src/act/act.dart index bd0d0614..7fe92c8e 100644 --- a/lib/src/act/act.dart +++ b/lib/src/act/act.dart @@ -4,7 +4,6 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; -import 'package:spot/src/screenshot/screenshot.dart'; import 'package:spot/src/spot/snapshot.dart'; /// Top level entry point to interact with widgets on the screen. From fc41b699f7c62553dfce3e7019cd145e80d308cc Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Wed, 22 May 2024 15:20:08 +0200 Subject: [PATCH 015/119] Add timeline experiement --- lib/src/act/act.dart | 23 +++--- lib/src/screenshot/screenshot.dart | 16 ++-- lib/src/timeline/timeline.dart | 121 +++++++++++++++++++++++++++++ test/timeline/timeline_test.dart | 40 ++++++++++ 4 files changed, 184 insertions(+), 16 deletions(-) create mode 100644 lib/src/timeline/timeline.dart create mode 100644 test/timeline/timeline_test.dart diff --git a/lib/src/act/act.dart b/lib/src/act/act.dart index 7fe92c8e..abb376d7 100644 --- a/lib/src/act/act.dart +++ b/lib/src/act/act.dart @@ -5,6 +5,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import 'package:spot/src/spot/snapshot.dart'; +import 'package:spot/src/timeline/timeline.dart'; /// Top level entry point to interact with widgets on the screen. /// @@ -72,10 +73,7 @@ class Act { /// Triggers a tap event on a given widget. /// If [showTapPosition] is true, a crosshair is drawn on the screenshot at /// the position of the tap. - Future tap( - WidgetSelector selector, { - bool? showTapPosition, - }) async { + Future tap(WidgetSelector selector) async { // Check if widget is in the widget tree. Throws if not. final snapshot = selector.snapshot()..existsOnce(); @@ -103,6 +101,16 @@ class Act { final centerPosition = renderObject.localToGlobal(renderObject.size.center(Offset.zero)); + final timeline = currentTimeline(); + if (timeline.mode != TimelineMode.off) { + final screenshot = + await takeScreenshotWithCrosshair(centerPosition: centerPosition); + timeline.addScreenshot( + screenshot, + name: 'Tap ${selector.toStringBreadcrumb()}', + ); + } + // Before tapping the widget, we need to make sure that the widget is not // covered by another widget, or outside the viewport. _pokeRenderObject( @@ -121,13 +129,6 @@ class Act { binding.handlePointerEvent(upEvent); await binding.pump(); - - if (showTapPosition == true) { - await takeScreenshotWithCrosshair( - element: element, - centerPosition: centerPosition, - ); - } }); }); } diff --git a/lib/src/screenshot/screenshot.dart b/lib/src/screenshot/screenshot.dart index 500c274e..b6c3911f 100644 --- a/lib/src/screenshot/screenshot.dart +++ b/lib/src/screenshot/screenshot.dart @@ -84,6 +84,7 @@ Future takeScreenshotWithCrosshair({ selector: selector, name: name, annotator: CrosshairAnnotator(centerPosition: centerPosition), + printToConsole: false, ); } @@ -93,6 +94,7 @@ Future _createScreenshot({ WidgetSelector? selector, String? name, ScreenshotAnnotator? annotator, + bool printToConsole = true, }) async { final binding = TestWidgetsFlutterBinding.instance; final Frame? frame = _caller(); @@ -195,11 +197,14 @@ Future _createScreenshot({ final file = spotTempDir.file(screenshotFileName); file.writeAsBytesSync(image); -// ignore: avoid_print - core.print( - 'Screenshot file://${file.path}\n' - ' taken at ${frame?.member} ${frame?.uri}:${frame?.line}:${frame?.column}', - ); + + if (printToConsole) { + // ignore: avoid_print + core.print( + 'Screenshot file://${file.path}\n' + ' taken at ${frame?.member} ${frame?.uri}:${frame?.line}:${frame?.column}', + ); + } return Screenshot(file: file, initiator: frame); } @@ -274,6 +279,7 @@ Frame? _caller({StackTrace? stack}) { if (line.isCore) return false; final url = line.uri.toString(); if (url.contains('package:spot')) return false; + if (url.startsWith('package:flutter_test')) return false; return true; }).toList(); final Frame? bestGuess = relevantLines.firstOrNull; diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart new file mode 100644 index 00000000..576e477f --- /dev/null +++ b/lib/src/timeline/timeline.dart @@ -0,0 +1,121 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/src/screenshot/screenshot.dart'; +import 'package:spot/src/spot/tree_snapshot.dart'; +import 'package:stack_trace/stack_trace.dart'; +import 'package:test_api/src/backend/invoker.dart'; +import 'package:test_api/src/backend/live_test.dart'; + +final Map _timelines = {}; + +void liveTimeline() { + print('馃敶 Live timeline recording'); + final timeline = currentTimeline(); + timeline.mode = TimelineMode.live; +} + +void printTimelineOnError() { + print('馃敶 timeline recording in case of error'); + final timeline = currentTimeline(); + timeline.mode = TimelineMode.record; +} + +void stopTimeline() { + print('Timeline recording is disabled'); + final timeline = currentTimeline(); + timeline.mode = TimelineMode.off; +} + +Timeline currentTimeline() { + final test = Invoker.current?.liveTest; + if (test == null) { + throw StateError('currentTimeline() must be called within a test'); + } + final timeline = _timelines[test]; + if (timeline != null) { + // existing timeline + return timeline; + } + + // create new timeline + final newTimeline = Timeline(); + + Invoker.current!.addTearDown(() { + if (newTimeline.mode == TimelineMode.record && + !test.state.result.isPassing) { + newTimeline.printToConsole(); + } + }); + _timelines[test] = newTimeline; + return newTimeline; +} + +class Timeline { + final List _events = []; + + TimelineMode mode = TimelineMode.off; + + void addScreenshot(Screenshot screenshot, {String? name}) { + addEvent(TimelineEvent( + name: name, + screenshot: screenshot, + timestamp: DateTime.now(), + treeSnapshot: currentWidgetTreeSnapshot(), + initiator: screenshot.initiator, + )); + } + + void addEvent(TimelineEvent event) { + _events.add(event); + if (mode == TimelineMode.live) { + _printEvent(event); + } + } + + void printToConsole() { + print('Timeline'); + for (final event in _events) { + _printEvent(event); + } + } + + void _printEvent(TimelineEvent event) { + final StringBuffer buffer = StringBuffer(); + final frame = event.initiator; + final caller = frame != null + ? 'at ${frame.member} ${frame.uri}:${frame.line}:${frame.column}' + : null; + + buffer.write('${event.name}'); + if (caller != null) { + buffer.write(' $caller'); + } + + if (event.screenshot != null) { + buffer.write('\nScreenshot: file://${event.screenshot!.file.path}'); + } + + print(buffer); + } +} + +class TimelineEvent { + final Screenshot? screenshot; + final String? name; + final DateTime timestamp; + final WidgetTreeSnapshot treeSnapshot; + final Frame? initiator; + + const TimelineEvent({ + this.screenshot, + this.name, + this.initiator, + required this.timestamp, + required this.treeSnapshot, + }); +} + +enum TimelineMode { + live, + record, + off, +} diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart new file mode 100644 index 00000000..68ed5ef0 --- /dev/null +++ b/test/timeline/timeline_test.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; +import 'package:spot/src/timeline/timeline.dart'; + +void main() { + testWidgets('Automatically take screenshots for timeline', (tester) async { + liveTimeline(); + stopTimeline(); + printTimelineOnError(); + int counter = 0; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + appBar: AppBar( + backgroundColor: Colors.blueAccent, + title: const Text('Home'), + actions: [ + IconButton( + icon: const Icon(Icons.home), + onPressed: () { + counter++; + }, + ), + ], + ), + ), + ), + ); + print('Before tap'); + await act.tap(spotIcon(Icons.home)); + print('After tap'); + + print('Before tap2'); + await act.tap(spot()); + print('After tap2'); + + expect(counter, 2); + }); +} From 4e574f3787bff48137d72f7e87e1c842eefd6bf1 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 22 May 2024 16:59:38 +0200 Subject: [PATCH 016/119] Add docs and improve naming --- lib/src/timeline/timeline.dart | 95 +++++++++++++++++++++++++------- test/timeline/timeline_test.dart | 4 +- 2 files changed, 76 insertions(+), 23 deletions(-) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index 576e477f..a23f4039 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -1,30 +1,38 @@ -import 'package:flutter_test/flutter_test.dart'; import 'package:spot/src/screenshot/screenshot.dart'; import 'package:spot/src/spot/tree_snapshot.dart'; -import 'package:stack_trace/stack_trace.dart'; +//ignore: implementation_imports import 'package:test_api/src/backend/invoker.dart'; +//ignore: implementation_imports import 'package:test_api/src/backend/live_test.dart'; final Map _timelines = {}; -void liveTimeline() { - print('馃敶 Live timeline recording'); +/// Starts the timeline recording and prints events as they happen. +void startLiveTimeline() { + // ignore: avoid_print + print('馃敶 - Recording timeline with live output'); final timeline = currentTimeline(); timeline.mode = TimelineMode.live; } -void printTimelineOnError() { - print('馃敶 timeline recording in case of error'); +/// Records the timeline but only prints it in case of an error. +void startOnErrorTimeline() { + // ignore: avoid_print + print('馃敶 - Recording timeline for error output'); final timeline = currentTimeline(); timeline.mode = TimelineMode.record; } +/// Stops the timeline recording. void stopTimeline() { - print('Timeline recording is disabled'); + // ignore: avoid_print + print('鈴革笌 - Timeline stopped'); final timeline = currentTimeline(); timeline.mode = TimelineMode.off; } +/// Returns the current timeline for the test or creates a new one if +/// it doesn't exist. Timeline currentTimeline() { final test = Invoker.current?.liveTest; if (test == null) { @@ -49,29 +57,38 @@ Timeline currentTimeline() { return newTimeline; } +/// A timeline of events that occurred during a test. class Timeline { final List _events = []; + /// The mode of the timeline. Defaults to [TimelineMode.off]. TimelineMode mode = TimelineMode.off; + /// Adds a screenshot to the timeline. void addScreenshot(Screenshot screenshot, {String? name}) { - addEvent(TimelineEvent( - name: name, - screenshot: screenshot, - timestamp: DateTime.now(), - treeSnapshot: currentWidgetTreeSnapshot(), - initiator: screenshot.initiator, - )); + addEvent( + TimelineEvent.now( + name: name, + screenshot: screenshot, + initiator: screenshot.initiator, + ), + ); } + /// Adds an event to the timeline. void addEvent(TimelineEvent event) { + if (mode == TimelineMode.off) { + return; + } _events.add(event); if (mode == TimelineMode.live) { _printEvent(event); } } + /// Prints the timeline to the console. void printToConsole() { + // ignore: avoid_print print('Timeline'); for (final event in _events) { _printEvent(event); @@ -93,18 +110,14 @@ class Timeline { if (event.screenshot != null) { buffer.write('\nScreenshot: file://${event.screenshot!.file.path}'); } - +// ignore: avoid_print print(buffer); } } +/// An event that occurred during a test. class TimelineEvent { - final Screenshot? screenshot; - final String? name; - final DateTime timestamp; - final WidgetTreeSnapshot treeSnapshot; - final Frame? initiator; - + /// Creates a new timeline event. const TimelineEvent({ this.screenshot, this.name, @@ -112,10 +125,50 @@ class TimelineEvent { required this.timestamp, required this.treeSnapshot, }); + + /// Creates a new timeline event with the current time and widget tree snapshot. + factory TimelineEvent.now({ + Screenshot? screenshot, + String? name, + Frame? initiator, + }) { + return TimelineEvent( + screenshot: screenshot, + name: name, + initiator: initiator, + timestamp: DateTime.now(), + treeSnapshot: currentWidgetTreeSnapshot(), + ); + } + + /// The screenshot taken at the time of the event. + final Screenshot? screenshot; + + /// The name of the event. + final String? name; + + /// The time at which the event occurred. + final DateTime timestamp; + + /// The widget tree snapshot at the time of the event. + final WidgetTreeSnapshot treeSnapshot; + + /// The frame that initiated the event. + final Frame? initiator; } +/// The mode of the timeline. +/// Available modes: +/// - [TimelineMode.live] - The timeline is recording and printing events as they happen. +/// - [TimelineMode.record] - The timeline is recording but not printing events unless the test fails. +/// - [TimelineMode.off] - The timeline is not recording. enum TimelineMode { + /// The timeline is recording and printing events as they happen. live, + + /// The timeline is recording but not printing events unless the test fails. record, + + /// The timeline is not recording. off, } diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index 68ed5ef0..87fb38a6 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -5,9 +5,9 @@ import 'package:spot/src/timeline/timeline.dart'; void main() { testWidgets('Automatically take screenshots for timeline', (tester) async { - liveTimeline(); + startLiveTimeline(); stopTimeline(); - printTimelineOnError(); + startOnErrorTimeline(); int counter = 0; await tester.pumpWidget( MaterialApp( From 1afb4bd16bf17b3e815329bf5d884f88068ec733 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 22 May 2024 17:37:34 +0200 Subject: [PATCH 017/119] Return timeline --- lib/spot.dart | 3 +-- lib/src/act/act.dart | 1 + lib/src/timeline/timeline.dart | 38 +++++++++++++++++++++++++++------- test/spot/screenshot_test.dart | 1 + 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/lib/spot.dart b/lib/spot.dart index dca6eb93..ab46ea60 100644 --- a/lib/spot.dart +++ b/lib/spot.dart @@ -23,8 +23,7 @@ export 'package:spot/src/screenshot/screenshot.dart' Screenshot, SelectorScreenshotExtension, SnapshotScreenshotExtension, - takeScreenshot, - takeScreenshotWithCrosshair; + takeScreenshot; export 'package:spot/src/spot/default_selectors.dart' show DefaultWidgetMatchers, DefaultWidgetSelectors; export 'package:spot/src/spot/diagnostic_props.dart' diff --git a/lib/src/act/act.dart b/lib/src/act/act.dart index abb376d7..420454ca 100644 --- a/lib/src/act/act.dart +++ b/lib/src/act/act.dart @@ -4,6 +4,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; +import 'package:spot/src/screenshot/screenshot.dart'; import 'package:spot/src/spot/snapshot.dart'; import 'package:spot/src/timeline/timeline.dart'; diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index a23f4039..38e8772c 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -8,27 +8,30 @@ import 'package:test_api/src/backend/live_test.dart'; final Map _timelines = {}; /// Starts the timeline recording and prints events as they happen. -void startLiveTimeline() { +Timeline liveTimeline() { // ignore: avoid_print print('馃敶 - Recording timeline with live output'); final timeline = currentTimeline(); - timeline.mode = TimelineMode.live; + timeline.startLiveTimeline(); + return timeline; } /// Records the timeline but only prints it in case of an error. -void startOnErrorTimeline() { +Timeline startOnErrorTimeline() { // ignore: avoid_print print('馃敶 - Recording timeline for error output'); final timeline = currentTimeline(); - timeline.mode = TimelineMode.record; + timeline.startOnErrorTimeLine(); + return timeline; } /// Stops the timeline recording. -void stopTimeline() { +Timeline stopTimeline() { // ignore: avoid_print print('鈴革笌 - Timeline stopped'); final timeline = currentTimeline(); - timeline.mode = TimelineMode.off; + timeline.stopTimeLine(); + return timeline; } /// Returns the current timeline for the test or creates a new one if @@ -61,8 +64,29 @@ Timeline currentTimeline() { class Timeline { final List _events = []; + /// The events that occurred during the test. + List get events => _events; + /// The mode of the timeline. Defaults to [TimelineMode.off]. - TimelineMode mode = TimelineMode.off; + TimelineMode _mode = TimelineMode.off; + + /// The current mode of the timeline. + TimelineMode get mode => _mode; + + /// Stops the timeline recording and printing. + void stopTimeLine() { + _mode = TimelineMode.off; + } + + /// Starts the timeline recording and prints events as they happen. + void startLiveTimeline() { + _mode = TimelineMode.live; + } + + /// Records the timeline but only prints it in case of an error. + void startOnErrorTimeLine() { + _mode = TimelineMode.record; + } /// Adds a screenshot to the timeline. void addScreenshot(Screenshot screenshot, {String? name}) { diff --git a/test/spot/screenshot_test.dart b/test/spot/screenshot_test.dart index 5806c1c5..fec750a7 100644 --- a/test/spot/screenshot_test.dart +++ b/test/spot/screenshot_test.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image/image.dart' as img; import 'package:spot/spot.dart'; +import 'package:spot/src/screenshot/screenshot.dart'; import '../util/assert_error.dart'; void main() { From eba534ffa2d3df9631f871f55eadaa132e8ee675 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 22 May 2024 17:45:20 +0200 Subject: [PATCH 018/119] Add test "Live timeline with added screenshot event" --- lib/src/timeline/timeline.dart | 24 +++++------ test/timeline/timeline_test.dart | 72 ++++++++++++++++++++++---------- 2 files changed, 61 insertions(+), 35 deletions(-) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index 38e8772c..99e7a288 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -9,26 +9,20 @@ final Map _timelines = {}; /// Starts the timeline recording and prints events as they happen. Timeline liveTimeline() { - // ignore: avoid_print - print('馃敶 - Recording timeline with live output'); final timeline = currentTimeline(); - timeline.startLiveTimeline(); + timeline.startWithLiveOutput(); return timeline; } /// Records the timeline but only prints it in case of an error. -Timeline startOnErrorTimeline() { - // ignore: avoid_print - print('馃敶 - Recording timeline for error output'); +Timeline onErrorTimeline() { final timeline = currentTimeline(); - timeline.startOnErrorTimeLine(); + timeline.startWithErrorOutput(); return timeline; } /// Stops the timeline recording. -Timeline stopTimeline() { - // ignore: avoid_print - print('鈴革笌 - Timeline stopped'); +Timeline stoppedTimeLine() { final timeline = currentTimeline(); timeline.stopTimeLine(); return timeline; @@ -75,16 +69,22 @@ class Timeline { /// Stops the timeline recording and printing. void stopTimeLine() { + // ignore: avoid_print + print('鈴革笌 - Timeline stopped'); _mode = TimelineMode.off; } /// Starts the timeline recording and prints events as they happen. - void startLiveTimeline() { + void startWithLiveOutput() { + // ignore: avoid_print + print('馃敶 - Recording timeline with live output'); _mode = TimelineMode.live; } /// Records the timeline but only prints it in case of an error. - void startOnErrorTimeLine() { + void startWithErrorOutput() { + // ignore: avoid_print + print('馃敶 - Recording timeline for error output'); _mode = TimelineMode.record; } diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index 87fb38a6..891aa10e 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -3,38 +3,64 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import 'package:spot/src/timeline/timeline.dart'; +import '../spot/screenshot_test.dart'; + void main() { - testWidgets('Automatically take screenshots for timeline', (tester) async { - startLiveTimeline(); - stopTimeline(); - startOnErrorTimeline(); - int counter = 0; + testWidgets('Live timeline with added screenshot event', (tester) async { + final Timeline timeLine = liveTimeline(); + tester.view.physicalSize = const Size(210, 210); + tester.view.devicePixelRatio = 1.0; + int i = 0; await tester.pumpWidget( - MaterialApp( - home: Scaffold( + _TimelineTestWidget( + onTap: () { + i++; + }, + ), + ); + + final button = spotIcon(Icons.home)..existsOnce(); + await act.tap(button); + expect(i, 1); + final shot = timeLine.events.first.screenshot!; + expect(shot.file.existsSync(), isTrue); + + final cyanPixelCoverage = + await percentageOfPixelsWithColor(shot.file, const Color(0xFF00FFFF)); + expect(cyanPixelCoverage, greaterThan(0.0)); + }); +} + +class _TimelineTestWidget extends StatelessWidget { + const _TimelineTestWidget({ + required this.onTap, + }); + + final void Function() onTap; + + @override + Widget build(BuildContext context) { + return MaterialApp( + builder: (context, child) { + return Scaffold( appBar: AppBar( - backgroundColor: Colors.blueAccent, title: const Text('Home'), actions: [ IconButton( icon: const Icon(Icons.home), - onPressed: () { - counter++; - }, + onPressed: onTap, ), ], ), - ), - ), + body: Center( + child: Container( + height: 200, + width: 200, + color: Colors.red, + ), + ), + ); + }, ); - print('Before tap'); - await act.tap(spotIcon(Icons.home)); - print('After tap'); - - print('Before tap2'); - await act.tap(spot()); - print('After tap2'); - - expect(counter, 2); - }); + } } From 0ea2f7a637a06a5e35996d8f01cb771a968729b1 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 22 May 2024 17:45:44 +0200 Subject: [PATCH 019/119] Add test "Cease recording live timeline after first tap" --- test/timeline/timeline_test.dart | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index 891aa10e..59842016 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -29,6 +29,32 @@ void main() { await percentageOfPixelsWithColor(shot.file, const Color(0xFF00FFFF)); expect(cyanPixelCoverage, greaterThan(0.0)); }); + + testWidgets('Cease recording live timeline after first tap', (tester) async { + final Timeline timeline = liveTimeline(); + tester.view.physicalSize = const Size(210, 210); + tester.view.devicePixelRatio = 1.0; + int i = 0; + await tester.pumpWidget( + _TimelineTestWidget( + onTap: () { + i++; + }, + ), + ); + + final button = spotIcon(Icons.home)..existsOnce(); + await act.tap(button); + expect(i, 1); + expect(timeline.events.length, 1); + await act.tap(button); + expect(i, 2); + expect(timeline.events.length, 2); + timeline.stopTimeLine(); + await act.tap(button); + expect(i, 3); + expect(timeline.events.length, 2); + }); } class _TimelineTestWidget extends StatelessWidget { From 60835ad09bd06172628aecbc1dd5ec58460f32e2 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 24 May 2024 11:29:07 +0200 Subject: [PATCH 020/119] Revert "Add test "Cease recording live timeline after first tap"" This reverts commit 0ea2f7a637a06a5e35996d8f01cb771a968729b1. --- test/timeline/timeline_test.dart | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index 59842016..891aa10e 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -29,32 +29,6 @@ void main() { await percentageOfPixelsWithColor(shot.file, const Color(0xFF00FFFF)); expect(cyanPixelCoverage, greaterThan(0.0)); }); - - testWidgets('Cease recording live timeline after first tap', (tester) async { - final Timeline timeline = liveTimeline(); - tester.view.physicalSize = const Size(210, 210); - tester.view.devicePixelRatio = 1.0; - int i = 0; - await tester.pumpWidget( - _TimelineTestWidget( - onTap: () { - i++; - }, - ), - ); - - final button = spotIcon(Icons.home)..existsOnce(); - await act.tap(button); - expect(i, 1); - expect(timeline.events.length, 1); - await act.tap(button); - expect(i, 2); - expect(timeline.events.length, 2); - timeline.stopTimeLine(); - await act.tap(button); - expect(i, 3); - expect(timeline.events.length, 2); - }); } class _TimelineTestWidget extends StatelessWidget { From c8a10c60ca0c30cbfe760c5005a387f4eeb5f112 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 24 May 2024 11:29:32 +0200 Subject: [PATCH 021/119] Revert "Add test "Live timeline with added screenshot event"" This reverts commit eba534ffa2d3df9631f871f55eadaa132e8ee675. --- lib/src/timeline/timeline.dart | 24 +++++------ test/timeline/timeline_test.dart | 72 ++++++++++---------------------- 2 files changed, 35 insertions(+), 61 deletions(-) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index 99e7a288..38e8772c 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -9,20 +9,26 @@ final Map _timelines = {}; /// Starts the timeline recording and prints events as they happen. Timeline liveTimeline() { + // ignore: avoid_print + print('馃敶 - Recording timeline with live output'); final timeline = currentTimeline(); - timeline.startWithLiveOutput(); + timeline.startLiveTimeline(); return timeline; } /// Records the timeline but only prints it in case of an error. -Timeline onErrorTimeline() { +Timeline startOnErrorTimeline() { + // ignore: avoid_print + print('馃敶 - Recording timeline for error output'); final timeline = currentTimeline(); - timeline.startWithErrorOutput(); + timeline.startOnErrorTimeLine(); return timeline; } /// Stops the timeline recording. -Timeline stoppedTimeLine() { +Timeline stopTimeline() { + // ignore: avoid_print + print('鈴革笌 - Timeline stopped'); final timeline = currentTimeline(); timeline.stopTimeLine(); return timeline; @@ -69,22 +75,16 @@ class Timeline { /// Stops the timeline recording and printing. void stopTimeLine() { - // ignore: avoid_print - print('鈴革笌 - Timeline stopped'); _mode = TimelineMode.off; } /// Starts the timeline recording and prints events as they happen. - void startWithLiveOutput() { - // ignore: avoid_print - print('馃敶 - Recording timeline with live output'); + void startLiveTimeline() { _mode = TimelineMode.live; } /// Records the timeline but only prints it in case of an error. - void startWithErrorOutput() { - // ignore: avoid_print - print('馃敶 - Recording timeline for error output'); + void startOnErrorTimeLine() { _mode = TimelineMode.record; } diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index 891aa10e..87fb38a6 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -3,64 +3,38 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import 'package:spot/src/timeline/timeline.dart'; -import '../spot/screenshot_test.dart'; - void main() { - testWidgets('Live timeline with added screenshot event', (tester) async { - final Timeline timeLine = liveTimeline(); - tester.view.physicalSize = const Size(210, 210); - tester.view.devicePixelRatio = 1.0; - int i = 0; + testWidgets('Automatically take screenshots for timeline', (tester) async { + startLiveTimeline(); + stopTimeline(); + startOnErrorTimeline(); + int counter = 0; await tester.pumpWidget( - _TimelineTestWidget( - onTap: () { - i++; - }, - ), - ); - - final button = spotIcon(Icons.home)..existsOnce(); - await act.tap(button); - expect(i, 1); - final shot = timeLine.events.first.screenshot!; - expect(shot.file.existsSync(), isTrue); - - final cyanPixelCoverage = - await percentageOfPixelsWithColor(shot.file, const Color(0xFF00FFFF)); - expect(cyanPixelCoverage, greaterThan(0.0)); - }); -} - -class _TimelineTestWidget extends StatelessWidget { - const _TimelineTestWidget({ - required this.onTap, - }); - - final void Function() onTap; - - @override - Widget build(BuildContext context) { - return MaterialApp( - builder: (context, child) { - return Scaffold( + MaterialApp( + home: Scaffold( appBar: AppBar( + backgroundColor: Colors.blueAccent, title: const Text('Home'), actions: [ IconButton( icon: const Icon(Icons.home), - onPressed: onTap, + onPressed: () { + counter++; + }, ), ], ), - body: Center( - child: Container( - height: 200, - width: 200, - color: Colors.red, - ), - ), - ); - }, + ), + ), ); - } + print('Before tap'); + await act.tap(spotIcon(Icons.home)); + print('After tap'); + + print('Before tap2'); + await act.tap(spot()); + print('After tap2'); + + expect(counter, 2); + }); } From 14857c840f3dc1ebb1a38af5ed9965bc6cb4973f Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 24 May 2024 11:29:48 +0200 Subject: [PATCH 022/119] Revert "Return timeline" This reverts commit 1afb4bd16bf17b3e815329bf5d884f88068ec733. --- lib/spot.dart | 3 ++- lib/src/act/act.dart | 1 - lib/src/timeline/timeline.dart | 38 +++++++--------------------------- test/spot/screenshot_test.dart | 1 - 4 files changed, 9 insertions(+), 34 deletions(-) diff --git a/lib/spot.dart b/lib/spot.dart index ab46ea60..dca6eb93 100644 --- a/lib/spot.dart +++ b/lib/spot.dart @@ -23,7 +23,8 @@ export 'package:spot/src/screenshot/screenshot.dart' Screenshot, SelectorScreenshotExtension, SnapshotScreenshotExtension, - takeScreenshot; + takeScreenshot, + takeScreenshotWithCrosshair; export 'package:spot/src/spot/default_selectors.dart' show DefaultWidgetMatchers, DefaultWidgetSelectors; export 'package:spot/src/spot/diagnostic_props.dart' diff --git a/lib/src/act/act.dart b/lib/src/act/act.dart index 420454ca..abb376d7 100644 --- a/lib/src/act/act.dart +++ b/lib/src/act/act.dart @@ -4,7 +4,6 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; -import 'package:spot/src/screenshot/screenshot.dart'; import 'package:spot/src/spot/snapshot.dart'; import 'package:spot/src/timeline/timeline.dart'; diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index 38e8772c..a23f4039 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -8,30 +8,27 @@ import 'package:test_api/src/backend/live_test.dart'; final Map _timelines = {}; /// Starts the timeline recording and prints events as they happen. -Timeline liveTimeline() { +void startLiveTimeline() { // ignore: avoid_print print('馃敶 - Recording timeline with live output'); final timeline = currentTimeline(); - timeline.startLiveTimeline(); - return timeline; + timeline.mode = TimelineMode.live; } /// Records the timeline but only prints it in case of an error. -Timeline startOnErrorTimeline() { +void startOnErrorTimeline() { // ignore: avoid_print print('馃敶 - Recording timeline for error output'); final timeline = currentTimeline(); - timeline.startOnErrorTimeLine(); - return timeline; + timeline.mode = TimelineMode.record; } /// Stops the timeline recording. -Timeline stopTimeline() { +void stopTimeline() { // ignore: avoid_print print('鈴革笌 - Timeline stopped'); final timeline = currentTimeline(); - timeline.stopTimeLine(); - return timeline; + timeline.mode = TimelineMode.off; } /// Returns the current timeline for the test or creates a new one if @@ -64,29 +61,8 @@ Timeline currentTimeline() { class Timeline { final List _events = []; - /// The events that occurred during the test. - List get events => _events; - /// The mode of the timeline. Defaults to [TimelineMode.off]. - TimelineMode _mode = TimelineMode.off; - - /// The current mode of the timeline. - TimelineMode get mode => _mode; - - /// Stops the timeline recording and printing. - void stopTimeLine() { - _mode = TimelineMode.off; - } - - /// Starts the timeline recording and prints events as they happen. - void startLiveTimeline() { - _mode = TimelineMode.live; - } - - /// Records the timeline but only prints it in case of an error. - void startOnErrorTimeLine() { - _mode = TimelineMode.record; - } + TimelineMode mode = TimelineMode.off; /// Adds a screenshot to the timeline. void addScreenshot(Screenshot screenshot, {String? name}) { diff --git a/test/spot/screenshot_test.dart b/test/spot/screenshot_test.dart index fec750a7..5806c1c5 100644 --- a/test/spot/screenshot_test.dart +++ b/test/spot/screenshot_test.dart @@ -8,7 +8,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image/image.dart' as img; import 'package:spot/spot.dart'; -import 'package:spot/src/screenshot/screenshot.dart'; import '../util/assert_error.dart'; void main() { From 6223c18017a7410fb31d263b5515c07219c1d255 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 24 May 2024 12:36:27 +0200 Subject: [PATCH 023/119] Add "live timeline" test --- test/timeline/timeline_test.dart | 88 ++++++++++++++++++++++---------- 1 file changed, 61 insertions(+), 27 deletions(-) diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index 87fb38a6..2ef1118d 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -1,40 +1,74 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import 'package:spot/src/timeline/timeline.dart'; +Iterable _screenshotMessageMatcher(String outPut) => + RegExp('Screenshot: file:').allMatches(outPut); + void main() { - testWidgets('Automatically take screenshots for timeline', (tester) async { - startLiveTimeline(); - stopTimeline(); - startOnErrorTimeline(); - int counter = 0; - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - appBar: AppBar( - backgroundColor: Colors.blueAccent, - title: const Text('Home'), - actions: [ - IconButton( - icon: const Icon(Icons.home), - onPressed: () { - counter++; - }, - ), - ], + testWidgets('Live timeline', (tester) async { + final addButtonSelector = spotIcon(Icons.add); + final subtractButtonSelector = spotIcon(Icons.remove); + + final output = await captureConsoleOutput(() async { + startLiveTimeline(); + int counter = 0; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + appBar: AppBar( + backgroundColor: Colors.blueAccent, + title: const Text('Home'), + actions: [ + IconButton( + icon: const Icon(Icons.add), + onPressed: () { + counter++; + }, + ), + IconButton( + icon: const Icon(Icons.remove), + onPressed: () { + counter--; + }, + ), + ], + ), ), ), - ), + ); + addButtonSelector.existsOnce(); + await act.tap(addButtonSelector); + expect(counter, 1); + await act.tap(subtractButtonSelector); + expect(counter, 0); + }); + + expect(output, contains('馃敶 - Recording timeline with live output')); + expect(output, contains('Tap ${addButtonSelector.toStringBreadcrumb()}')); + expect( + output, + contains('Tap ${subtractButtonSelector.toStringBreadcrumb()}'), ); - print('Before tap'); - await act.tap(spotIcon(Icons.home)); - print('After tap'); + expect(_screenshotMessageMatcher(output).length, 2); + }); +} - print('Before tap2'); - await act.tap(spot()); - print('After tap2'); +Future captureConsoleOutput( + Future Function() testFunction) async { + final StringBuffer buffer = StringBuffer(); + final ZoneSpecification spec = ZoneSpecification( + print: (self, parent, zone, line) { + buffer.writeln(line); + }, + ); - expect(counter, 2); + await Zone.current.fork(specification: spec).run(() async { + await testFunction(); }); + + return buffer.toString(); } From 600405179a0e44a90d432de57e0905fc4ab733f1 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 24 May 2024 12:37:00 +0200 Subject: [PATCH 024/119] Add "onError timeline test" --- test/timeline/timeline_test.dart | 49 ++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index 2ef1118d..9d65417e 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -55,6 +55,55 @@ void main() { ); expect(_screenshotMessageMatcher(output).length, 2); }); + testWidgets('OnError timeline', (tester) async { + final addButtonSelector = spotIcon(Icons.add); + final subtractButtonSelector = spotIcon(Icons.remove); + + final output = await captureConsoleOutput(() async { + startOnErrorTimeline(); + int counter = 0; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + appBar: AppBar( + backgroundColor: Colors.blueAccent, + title: const Text('Home'), + actions: [ + IconButton( + icon: const Icon(Icons.add), + onPressed: () { + counter++; + }, + ), + IconButton( + icon: const Icon(Icons.remove), + onPressed: () { + counter--; + }, + ), + ], + ), + ), + ), + ); + addButtonSelector.existsOnce(); + await act.tap(addButtonSelector); + expect(counter, 1); + await act.tap(subtractButtonSelector); + expect(counter, 0); + }); + + expect(output, contains('馃敶 - Recording timeline for error output')); + expect( + output, + isNot(contains('Tap ${addButtonSelector.toStringBreadcrumb()}')), + ); + expect( + output, + isNot(contains('Tap ${subtractButtonSelector.toStringBreadcrumb()}')), + ); + expect(_screenshotMessageMatcher(output).length, 0); + }); } Future captureConsoleOutput( From e0b75731eb92ffaa623d5f03533538b1ca1c650d Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 24 May 2024 12:40:32 +0200 Subject: [PATCH 025/119] Add test "Timeline Mode off" --- test/timeline/timeline_test.dart | 49 ++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index 9d65417e..63d7f585 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -104,6 +104,55 @@ void main() { ); expect(_screenshotMessageMatcher(output).length, 0); }); + testWidgets('Timeline Mode off', (tester) async { + final addButtonSelector = spotIcon(Icons.add); + final subtractButtonSelector = spotIcon(Icons.remove); + + final output = await captureConsoleOutput(() async { + stopTimeline(); + int counter = 0; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + appBar: AppBar( + backgroundColor: Colors.blueAccent, + title: const Text('Home'), + actions: [ + IconButton( + icon: const Icon(Icons.add), + onPressed: () { + counter++; + }, + ), + IconButton( + icon: const Icon(Icons.remove), + onPressed: () { + counter--; + }, + ), + ], + ), + ), + ), + ); + addButtonSelector.existsOnce(); + await act.tap(addButtonSelector); + expect(counter, 1); + await act.tap(subtractButtonSelector); + expect(counter, 0); + }); + + expect(output, contains('鈴革笌 - Timeline stopped')); + expect( + output, + isNot(contains('Tap ${addButtonSelector.toStringBreadcrumb()}')), + ); + expect( + output, + isNot(contains('Tap ${subtractButtonSelector.toStringBreadcrumb()}')), + ); + expect(_screenshotMessageMatcher(output).length, 0); + }); } Future captureConsoleOutput( From 63c0449a34050ee140c5c8199c9ab8fcd1d784a4 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 24 May 2024 13:14:06 +0200 Subject: [PATCH 026/119] Add test "Turn timeline mode off during test" --- test/timeline/timeline_test.dart | 227 ++++++++++++++++--------------- 1 file changed, 116 insertions(+), 111 deletions(-) diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index 63d7f585..636527ae 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -7,155 +7,160 @@ import 'package:spot/src/timeline/timeline.dart'; Iterable _screenshotMessageMatcher(String outPut) => RegExp('Screenshot: file:').allMatches(outPut); - +final _addButtonSelector = spotIcon(Icons.add); +final _subtractButtonSelector = spotIcon(Icons.remove); +final _clearButtonSelector = spotIcon(Icons.clear); void main() { testWidgets('Live timeline', (tester) async { - final addButtonSelector = spotIcon(Icons.add); - final subtractButtonSelector = spotIcon(Icons.remove); - - final output = await captureConsoleOutput(() async { + final output = await _captureConsoleOutput(() async { startLiveTimeline(); - int counter = 0; - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - appBar: AppBar( - backgroundColor: Colors.blueAccent, - title: const Text('Home'), - actions: [ - IconButton( - icon: const Icon(Icons.add), - onPressed: () { - counter++; - }, - ), - IconButton( - icon: const Icon(Icons.remove), - onPressed: () { - counter--; - }, - ), - ], - ), - ), - ), - ); - addButtonSelector.existsOnce(); - await act.tap(addButtonSelector); - expect(counter, 1); - await act.tap(subtractButtonSelector); - expect(counter, 0); + await tester.pumpWidget(const _TimelineTestWidget()); + _addButtonSelector.existsOnce(); + spotText('Counter: 3').existsOnce(); + await act.tap(_addButtonSelector); + spotText('Counter: 4').existsOnce(); + await act.tap(_subtractButtonSelector); + spotText('Counter: 3').existsOnce(); }); expect(output, contains('馃敶 - Recording timeline with live output')); - expect(output, contains('Tap ${addButtonSelector.toStringBreadcrumb()}')); + expect(output, contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')); expect( output, - contains('Tap ${subtractButtonSelector.toStringBreadcrumb()}'), + contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), ); expect(_screenshotMessageMatcher(output).length, 2); }); testWidgets('OnError timeline', (tester) async { - final addButtonSelector = spotIcon(Icons.add); - final subtractButtonSelector = spotIcon(Icons.remove); - - final output = await captureConsoleOutput(() async { + final output = await _captureConsoleOutput(() async { startOnErrorTimeline(); - int counter = 0; - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - appBar: AppBar( - backgroundColor: Colors.blueAccent, - title: const Text('Home'), - actions: [ - IconButton( - icon: const Icon(Icons.add), - onPressed: () { - counter++; - }, - ), - IconButton( - icon: const Icon(Icons.remove), - onPressed: () { - counter--; - }, - ), - ], - ), - ), - ), - ); - addButtonSelector.existsOnce(); - await act.tap(addButtonSelector); - expect(counter, 1); - await act.tap(subtractButtonSelector); - expect(counter, 0); + + await tester.pumpWidget(const _TimelineTestWidget()); + _addButtonSelector.existsOnce(); + spotText('Counter: 3').existsOnce(); + await act.tap(_addButtonSelector); + spotText('Counter: 4').existsOnce(); + await act.tap(_subtractButtonSelector); + spotText('Counter: 3').existsOnce(); }); expect(output, contains('馃敶 - Recording timeline for error output')); expect( output, - isNot(contains('Tap ${addButtonSelector.toStringBreadcrumb()}')), + isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), ); expect( output, - isNot(contains('Tap ${subtractButtonSelector.toStringBreadcrumb()}')), + isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), ); expect(_screenshotMessageMatcher(output).length, 0); }); - testWidgets('Timeline Mode off', (tester) async { - final addButtonSelector = spotIcon(Icons.add); - final subtractButtonSelector = spotIcon(Icons.remove); - - final output = await captureConsoleOutput(() async { + testWidgets('Start with Timeline Mode off', (tester) async { + final output = await _captureConsoleOutput(() async { stopTimeline(); - int counter = 0; - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - appBar: AppBar( - backgroundColor: Colors.blueAccent, - title: const Text('Home'), - actions: [ - IconButton( - icon: const Icon(Icons.add), - onPressed: () { - counter++; - }, - ), - IconButton( - icon: const Icon(Icons.remove), - onPressed: () { - counter--; - }, - ), - ], - ), - ), - ), - ); - addButtonSelector.existsOnce(); - await act.tap(addButtonSelector); - expect(counter, 1); - await act.tap(subtractButtonSelector); - expect(counter, 0); + await tester.pumpWidget(const _TimelineTestWidget()); + _addButtonSelector.existsOnce(); + spotText('Counter: 3').existsOnce(); + await act.tap(_addButtonSelector); + spotText('Counter: 4').existsOnce(); + await act.tap(_subtractButtonSelector); + spotText('Counter: 3').existsOnce(); }); expect(output, contains('鈴革笌 - Timeline stopped')); expect( output, - isNot(contains('Tap ${addButtonSelector.toStringBreadcrumb()}')), + isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), ); expect( output, - isNot(contains('Tap ${subtractButtonSelector.toStringBreadcrumb()}')), + isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), ); expect(_screenshotMessageMatcher(output).length, 0); }); + testWidgets('Turn timeline mode off during test', (tester) async { + final output = await _captureConsoleOutput(() async { + startLiveTimeline(); + await tester.pumpWidget( + const _TimelineTestWidget(), + ); + spotText('Counter: 3').existsOnce(); + _addButtonSelector.existsOnce(); + await act.tap(_addButtonSelector); + spotText('Counter: 4').existsOnce(); + await act.tap(_subtractButtonSelector); + spotText('Counter: 3').existsOnce(); + stopTimeline(); + await act.tap(_clearButtonSelector); + spotText('Counter: 0').existsOnce(); + }); + expect(output, contains('馃敶 - Recording timeline with live output')); + expect(output, contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')); + expect( + output, + contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), + ); + expect(output, contains('鈴革笌 - Timeline stopped')); + // No further events were added to the timeline, including screenshots + expect( + output, + isNot(contains('Tap ${_clearButtonSelector.toStringBreadcrumb()}')), + ); + expect(_screenshotMessageMatcher(output).length, 2); + }); +} + +class _TimelineTestWidget extends StatefulWidget { + const _TimelineTestWidget(); + + @override + State<_TimelineTestWidget> createState() => _TimelineTestWidgetState(); +} + +class _TimelineTestWidgetState extends State<_TimelineTestWidget> { + int _counter = 3; + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + backgroundColor: Colors.blueAccent, + title: const Text('Home'), + actions: [ + IconButton( + icon: const Icon(Icons.add), + onPressed: () { + setState(() { + _counter++; + }); + }, + ), + IconButton( + icon: const Icon(Icons.remove), + onPressed: () { + setState(() { + _counter--; + }); + }, + ), + IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + setState(() { + _counter = 0; + }); + }, + ), + ], + ), + body: Center(child: Text('Counter: $_counter')), + ), + ); + } } -Future captureConsoleOutput( +Future _captureConsoleOutput( Future Function() testFunction) async { final StringBuffer buffer = StringBuffer(); final ZoneSpecification spec = ZoneSpecification( From 89c8dfa8fe14394c7e3dabdd5b4bab14ba30829a Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Mon, 27 May 2024 17:30:14 +0200 Subject: [PATCH 027/119] Add test "OnError timeline - without error" --- pubspec.yaml | 1 + test/timeline/timeline_test.dart | 192 +++++++++++++++--------- test/timeline/timeline_test_widget.dart | 51 +++++++ 3 files changed, 171 insertions(+), 73 deletions(-) create mode 100644 test/timeline/timeline_test_widget.dart diff --git a/pubspec.yaml b/pubspec.yaml index efa19187..e956f192 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,6 +23,7 @@ dependencies: test: ^1.24.0 test_api: '>=0.5.0 <0.8.0' stack_trace: ^1.11.0 + test_process: ^2.1.0 dev_dependencies: image: ^4.0.0 diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index 636527ae..9df75e78 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -1,20 +1,25 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import 'package:spot/src/timeline/timeline.dart'; +import 'package:test_process/test_process.dart'; + +import 'timeline_test_widget.dart'; Iterable _screenshotMessageMatcher(String outPut) => RegExp('Screenshot: file:').allMatches(outPut); final _addButtonSelector = spotIcon(Icons.add); final _subtractButtonSelector = spotIcon(Icons.remove); final _clearButtonSelector = spotIcon(Icons.clear); + void main() { testWidgets('Live timeline', (tester) async { final output = await _captureConsoleOutput(() async { startLiveTimeline(); - await tester.pumpWidget(const _TimelineTestWidget()); + await tester.pumpWidget(const TimelineTestWidget()); _addButtonSelector.existsOnce(); spotText('Counter: 3').existsOnce(); await act.tap(_addButtonSelector); @@ -31,34 +36,10 @@ void main() { ); expect(_screenshotMessageMatcher(output).length, 2); }); - testWidgets('OnError timeline', (tester) async { - final output = await _captureConsoleOutput(() async { - startOnErrorTimeline(); - - await tester.pumpWidget(const _TimelineTestWidget()); - _addButtonSelector.existsOnce(); - spotText('Counter: 3').existsOnce(); - await act.tap(_addButtonSelector); - spotText('Counter: 4').existsOnce(); - await act.tap(_subtractButtonSelector); - spotText('Counter: 3').existsOnce(); - }); - - expect(output, contains('馃敶 - Recording timeline for error output')); - expect( - output, - isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), - ); - expect( - output, - isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), - ); - expect(_screenshotMessageMatcher(output).length, 0); - }); testWidgets('Start with Timeline Mode off', (tester) async { final output = await _captureConsoleOutput(() async { stopTimeline(); - await tester.pumpWidget(const _TimelineTestWidget()); + await tester.pumpWidget(const TimelineTestWidget()); _addButtonSelector.existsOnce(); spotText('Counter: 3').existsOnce(); await act.tap(_addButtonSelector); @@ -82,7 +63,7 @@ void main() { final output = await _captureConsoleOutput(() async { startLiveTimeline(); await tester.pumpWidget( - const _TimelineTestWidget(), + const TimelineTestWidget(), ); spotText('Counter: 3').existsOnce(); _addButtonSelector.existsOnce(); @@ -108,60 +89,125 @@ void main() { ); expect(_screenshotMessageMatcher(output).length, 2); }); -} -class _TimelineTestWidget extends StatefulWidget { - const _TimelineTestWidget(); + group('onError timeline', () { + testWidgets('OnError timeline - without error', (tester) async { + final output = await _captureConsoleOutput(() async { + startOnErrorTimeline(); + + await tester.pumpWidget(const TimelineTestWidget()); + _addButtonSelector.existsOnce(); + spotText('Counter: 3').existsOnce(); + await act.tap(_addButtonSelector); + spotText('Counter: 4').existsOnce(); + await act.tap(_subtractButtonSelector); + spotText('Counter: 3').existsOnce(); + }); + + expect(output, contains('馃敶 - Recording timeline for error output')); + expect( + output, + isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), + ); + expect( + output, + isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), + ); + expect(_screenshotMessageMatcher(output).length, 0); + }); - @override - State<_TimelineTestWidget> createState() => _TimelineTestWidgetState(); + test('OnError timeline - with error, prints timeline', () async { + const importPart = ''' +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; +import 'package:spot/src/timeline/timeline.dart'; +'''; + + final widgetPart = + File('test/timeline/timeline_test_widget.dart').readAsStringSync(); + + const testPart = ''' +void main() async { + final addButtonSelector = spotIcon(Icons.add); + final subtractButtonSelector = spotIcon(Icons.remove); + testWidgets('OnError timeline with error', (WidgetTester tester) async { + startOnErrorTimeline(); + await tester.pumpWidget(const TimelineTestWidget()); + addButtonSelector.existsOnce(); + spotText('Counter: 3').existsOnce(); + await act.tap(addButtonSelector); + spotText('Counter: 4').existsOnce(); + await act.tap(subtractButtonSelector); + spotText('Counter: 3').existsOnce(); + // Make test fail intentionally + spotText('Counter: 99').existsOnce(); + }); } +'''; + + final testParts = [importPart, widgetPart, testPart].join('\n'); + + final tempDir = Directory.systemTemp.createTempSync(); + final tempTestFile = File('${tempDir.path}/temp_test.dart'); + await tempTestFile.writeAsString(testParts); + + final testProcess = + await TestProcess.start('flutter', ['test', tempTestFile.path]); + + final stdoutBuffer = StringBuffer(); + + bool write = false; + await for (final line in testProcess.stdoutStream()) { + if (line == 'Timeline') { + write = true; + } + if (line.startsWith('To run this test again:')) { + write = false; + } + if (write) { + stdoutBuffer.writeln(line); + } + } -class _TimelineTestWidgetState extends State<_TimelineTestWidget> { - int _counter = 3; - - @override - Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - appBar: AppBar( - backgroundColor: Colors.blueAccent, - title: const Text('Home'), - actions: [ - IconButton( - icon: const Icon(Icons.add), - onPressed: () { - setState(() { - _counter++; - }); - }, - ), - IconButton( - icon: const Icon(Icons.remove), - onPressed: () { - setState(() { - _counter--; - }); - }, - ), - IconButton( - icon: const Icon(Icons.clear), - onPressed: () { - setState(() { - _counter = 0; - }); - }, - ), - ], + await testProcess.shouldExit(); + + tempDir.deleteSync(recursive: true); + + final stdout = stdoutBuffer.toString(); + + final timeline = stdout.split('\n'); + expect(timeline.first, 'Timeline'); + expect( + timeline[1].startsWith( + 'Tap Icon Widget with icon: "IconData(U+0E047)" at main. file:///', ), - body: Center(child: Text('Counter: $_counter')), - ), - ); - } + isTrue, + ); + expect( + timeline[2].startsWith( + 'Screenshot: file:///', + ), + isTrue, + ); + expect( + timeline[3].startsWith( + 'Tap Icon Widget with icon: "IconData(U+0E516)" at main. file:///', + ), + isTrue, + ); + expect( + timeline[4].startsWith( + 'Screenshot: file:///', + ), + isTrue, + ); + }); + }); } Future _captureConsoleOutput( - Future Function() testFunction) async { + Future Function() testFunction, +) async { final StringBuffer buffer = StringBuffer(); final ZoneSpecification spec = ZoneSpecification( print: (self, parent, zone, line) { diff --git a/test/timeline/timeline_test_widget.dart b/test/timeline/timeline_test_widget.dart new file mode 100644 index 00000000..759a44b0 --- /dev/null +++ b/test/timeline/timeline_test_widget.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +class TimelineTestWidget extends StatefulWidget { + const TimelineTestWidget({super.key}); + + @override + State createState() => _TimelineTestWidgetState(); +} + +class _TimelineTestWidgetState extends State { + int _counter = 3; + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + backgroundColor: Colors.blueAccent, + title: const Text('Home'), + actions: [ + IconButton( + icon: const Icon(Icons.add), + onPressed: () { + setState(() { + _counter++; + }); + }, + ), + IconButton( + icon: const Icon(Icons.remove), + onPressed: () { + setState(() { + _counter--; + }); + }, + ), + IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + setState(() { + _counter = 0; + }); + }, + ), + ], + ), + body: Center(child: Text('Counter: $_counter')), + ), + ); + } +} From 097864e0ed294b8dbbba46c42986fbd526c34ca0 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Mon, 27 May 2024 17:33:46 +0200 Subject: [PATCH 028/119] Add ignore line error --- lib/src/timeline/timeline.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index a23f4039..bdf30bbd 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -1,3 +1,4 @@ +// ignore_for_file: depend_on_referenced_packages import 'package:spot/src/screenshot/screenshot.dart'; import 'package:spot/src/spot/tree_snapshot.dart'; //ignore: implementation_imports From 308a4d7b4d814190d36963cc04a969b13e3a956a Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Mon, 27 May 2024 18:17:11 +0200 Subject: [PATCH 029/119] Improve doc and naming --- lib/src/timeline/timeline.dart | 2 +- test/timeline/timeline_test.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index bdf30bbd..678f7329 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -24,7 +24,7 @@ void startOnErrorTimeline() { timeline.mode = TimelineMode.record; } -/// Stops the timeline recording. +/// Stops the timeline from recording. void stopTimeline() { // ignore: avoid_print print('鈴革笌 - Timeline stopped'); diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index 9df75e78..8f51eb12 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -145,11 +145,11 @@ void main() async { } '''; - final testParts = [importPart, widgetPart, testPart].join('\n'); + final testAsString = [importPart, widgetPart, testPart].join('\n'); final tempDir = Directory.systemTemp.createTempSync(); final tempTestFile = File('${tempDir.path}/temp_test.dart'); - await tempTestFile.writeAsString(testParts); + await tempTestFile.writeAsString(testAsString); final testProcess = await TestProcess.start('flutter', ['test', tempTestFile.path]); From 64cb93b3f6100bdd3119419c141cd094ef089fd3 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Tue, 28 May 2024 11:56:39 +0200 Subject: [PATCH 030/119] Do not export takeScreenshotWithCrosshair --- lib/spot.dart | 3 +-- lib/src/act/act.dart | 1 + test/spot/screenshot_test.dart | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/spot.dart b/lib/spot.dart index dca6eb93..ab46ea60 100644 --- a/lib/spot.dart +++ b/lib/spot.dart @@ -23,8 +23,7 @@ export 'package:spot/src/screenshot/screenshot.dart' Screenshot, SelectorScreenshotExtension, SnapshotScreenshotExtension, - takeScreenshot, - takeScreenshotWithCrosshair; + takeScreenshot; export 'package:spot/src/spot/default_selectors.dart' show DefaultWidgetMatchers, DefaultWidgetSelectors; export 'package:spot/src/spot/diagnostic_props.dart' diff --git a/lib/src/act/act.dart b/lib/src/act/act.dart index abb376d7..420454ca 100644 --- a/lib/src/act/act.dart +++ b/lib/src/act/act.dart @@ -4,6 +4,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; +import 'package:spot/src/screenshot/screenshot.dart'; import 'package:spot/src/spot/snapshot.dart'; import 'package:spot/src/timeline/timeline.dart'; diff --git a/test/spot/screenshot_test.dart b/test/spot/screenshot_test.dart index 5806c1c5..fec750a7 100644 --- a/test/spot/screenshot_test.dart +++ b/test/spot/screenshot_test.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image/image.dart' as img; import 'package:spot/spot.dart'; +import 'package:spot/src/screenshot/screenshot.dart'; import '../util/assert_error.dart'; void main() { From a3e07d18d59af0589a2cab466e0307af369add01 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Tue, 28 May 2024 17:02:28 +0200 Subject: [PATCH 031/119] Improve naming, documentation, logging --- lib/src/act/act.dart | 7 ++++-- lib/src/timeline/timeline.dart | 42 +++++++++++++++++++++++--------- test/timeline/timeline_test.dart | 35 +++++++++++++++----------- 3 files changed, 57 insertions(+), 27 deletions(-) diff --git a/lib/src/act/act.dart b/lib/src/act/act.dart index 420454ca..1ed17eaa 100644 --- a/lib/src/act/act.dart +++ b/lib/src/act/act.dart @@ -72,8 +72,11 @@ class Act { } /// Triggers a tap event on a given widget. - /// If [showTapPosition] is true, a crosshair is drawn on the screenshot at - /// the position of the tap. + /// If a [Timeline] is running, an annotated screenshot, indicating the tap + /// position, is added to the timeline. + /// + /// See also: + /// - [Timeline] Future tap(WidgetSelector selector) async { // Check if widget is in the widget tree. Throws if not. final snapshot = selector.snapshot()..existsOnce(); diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index 678f7329..b04eccc8 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -8,27 +8,33 @@ import 'package:test_api/src/backend/live_test.dart'; final Map _timelines = {}; -/// Starts the timeline recording and prints events as they happen. -void startLiveTimeline() { - // ignore: avoid_print - print('馃敶 - Recording timeline with live output'); +/// Records the timeline and prints events as they happen. +void recordLiveTimeline() { final timeline = currentTimeline(); + final isRecordingLive = timeline.mode == TimelineMode.live; + final message = isRecordingLive ? 'Already recording' : 'Now recording'; + // ignore: avoid_print + print('馃敶 - $message live timeline'); timeline.mode = TimelineMode.live; } /// Records the timeline but only prints it in case of an error. -void startOnErrorTimeline() { - // ignore: avoid_print - print('馃敶 - Recording timeline for error output'); +void recordOnErrorTimeline() { final timeline = currentTimeline(); + final isRecordingError = timeline.mode == TimelineMode.record; + final message = isRecordingError ? 'Already' : 'Now'; + // ignore: avoid_print + print('馃敶 - $message recording error output timeline'); timeline.mode = TimelineMode.record; } /// Stops the timeline from recording. -void stopTimeline() { - // ignore: avoid_print - print('鈴革笌 - Timeline stopped'); +void stopRecordingTimeline() { final timeline = currentTimeline(); + final isRecording = timeline.mode != TimelineMode.off; + final message = isRecording ? 'stopped' : 'is off'; + // ignore: avoid_print + print('鈴革笌 - Timeline recording $message'); timeline.mode = TimelineMode.off; } @@ -58,7 +64,21 @@ Timeline currentTimeline() { return newTimeline; } -/// A timeline of events that occurred during a test. +/// Records a timeline of events during a test. +/// +/// Usage: +/// ```dart +/// testWidgets('Live timeline', (tester) async { +/// recordOnErrorTimeline(); // start recording the timeline, output on Error +/// recordLiveTimeline(); // start recording the timeline with live output +/// await tester.pumpWidget(const IncrementCounter()); +/// spotText('Counter: 0').existsOnce(); +/// final addButton = spotIcon(Icons.add)..existsOnce(); +/// await act.tap(addButton); +/// spotText('Counter: 1').existsOnce(); +/// stopRecordingTimeline(); // stops recording the timeline +/// }); +/// ``` class Timeline { final List _events = []; diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index 8f51eb12..b721e839 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -18,7 +18,7 @@ final _clearButtonSelector = spotIcon(Icons.clear); void main() { testWidgets('Live timeline', (tester) async { final output = await _captureConsoleOutput(() async { - startLiveTimeline(); + recordLiveTimeline(); await tester.pumpWidget(const TimelineTestWidget()); _addButtonSelector.existsOnce(); spotText('Counter: 3').existsOnce(); @@ -26,19 +26,21 @@ void main() { spotText('Counter: 4').existsOnce(); await act.tap(_subtractButtonSelector); spotText('Counter: 3').existsOnce(); + // Notify that the timeline of this type is already recording. + recordLiveTimeline(); }); - - expect(output, contains('馃敶 - Recording timeline with live output')); + expect(output, contains('馃敶 - Now recording live timeline')); expect(output, contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')); expect( output, contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), ); + expect(output, contains('馃敶 - Already recording live timeline')); expect(_screenshotMessageMatcher(output).length, 2); }); testWidgets('Start with Timeline Mode off', (tester) async { final output = await _captureConsoleOutput(() async { - stopTimeline(); + stopRecordingTimeline(); await tester.pumpWidget(const TimelineTestWidget()); _addButtonSelector.existsOnce(); spotText('Counter: 3').existsOnce(); @@ -48,7 +50,7 @@ void main() { spotText('Counter: 3').existsOnce(); }); - expect(output, contains('鈴革笌 - Timeline stopped')); + expect(output, contains('鈴革笌 - Timeline recording is off')); expect( output, isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), @@ -61,7 +63,7 @@ void main() { }); testWidgets('Turn timeline mode off during test', (tester) async { final output = await _captureConsoleOutput(() async { - startLiveTimeline(); + recordLiveTimeline(); await tester.pumpWidget( const TimelineTestWidget(), ); @@ -71,30 +73,33 @@ void main() { spotText('Counter: 4').existsOnce(); await act.tap(_subtractButtonSelector); spotText('Counter: 3').existsOnce(); - stopTimeline(); + // Notify that the recording stopped + stopRecordingTimeline(); await act.tap(_clearButtonSelector); spotText('Counter: 0').existsOnce(); + // Notify that the recording is off + stopRecordingTimeline(); }); - expect(output, contains('馃敶 - Recording timeline with live output')); + expect(output, contains('馃敶 - Now recording live timeline')); expect(output, contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')); expect( output, contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), ); - expect(output, contains('鈴革笌 - Timeline stopped')); + expect(output, contains('鈴革笌 - Timeline recording stopped')); // No further events were added to the timeline, including screenshots expect( output, isNot(contains('Tap ${_clearButtonSelector.toStringBreadcrumb()}')), ); expect(_screenshotMessageMatcher(output).length, 2); + expect(output, contains('鈴革笌 - Timeline recording is off')); }); group('onError timeline', () { testWidgets('OnError timeline - without error', (tester) async { final output = await _captureConsoleOutput(() async { - startOnErrorTimeline(); - + recordOnErrorTimeline(); await tester.pumpWidget(const TimelineTestWidget()); _addButtonSelector.existsOnce(); spotText('Counter: 3').existsOnce(); @@ -102,9 +107,10 @@ void main() { spotText('Counter: 4').existsOnce(); await act.tap(_subtractButtonSelector); spotText('Counter: 3').existsOnce(); + // Notify that the timeline of this type is already recording. + recordOnErrorTimeline(); }); - - expect(output, contains('馃敶 - Recording timeline for error output')); + expect(output, contains('馃敶 - Now recording error output timeline')); expect( output, isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), @@ -114,6 +120,7 @@ void main() { isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), ); expect(_screenshotMessageMatcher(output).length, 0); + expect(output, contains('馃敶 - Already recording error output timeline')); }); test('OnError timeline - with error, prints timeline', () async { @@ -131,7 +138,7 @@ void main() async { final addButtonSelector = spotIcon(Icons.add); final subtractButtonSelector = spotIcon(Icons.remove); testWidgets('OnError timeline with error', (WidgetTester tester) async { - startOnErrorTimeline(); + recordOnErrorTimeline(); await tester.pumpWidget(const TimelineTestWidget()); addButtonSelector.existsOnce(); spotText('Counter: 3').existsOnce(); From 55e00cd4e321acd6c8bcfa3da4df60b60bdcea96 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Tue, 28 May 2024 17:05:57 +0200 Subject: [PATCH 032/119] Export timeline methods --- lib/spot.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/spot.dart b/lib/spot.dart index ab46ea60..98ea1744 100644 --- a/lib/spot.dart +++ b/lib/spot.dart @@ -92,6 +92,8 @@ export 'package:spot/src/spot/widget_selector.dart' // ignore: deprecated_member_use_from_same_package SingleWidgetSelector, WidgetSelector; +export 'package:spot/src/timeline/timeline.dart' + show recordLiveTimeline, recordOnErrorTimeline, stopRecordingTimeline; export 'package:spot/src/widgets/align.g.dart'; export 'package:spot/src/widgets/anytext.g.dart'; export 'package:spot/src/widgets/circularprogressindicator.g.dart'; From f98428df40790bee372bc5b24d12fb3ed57f1a17 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Tue, 28 May 2024 17:06:23 +0200 Subject: [PATCH 033/119] Rm unused import --- test/timeline/timeline_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index b721e839..8e52a76d 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; -import 'package:spot/src/timeline/timeline.dart'; import 'package:test_process/test_process.dart'; import 'timeline_test_widget.dart'; From 79c91ccfdc65b7eac8788c1b8450019a5bf25af8 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Tue, 28 May 2024 18:32:11 +0200 Subject: [PATCH 034/119] Improve logging design --- lib/src/timeline/timeline.dart | 13 ++++++++----- test/timeline/timeline_test.dart | 26 +++++++++++++++----------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index b04eccc8..233453d6 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -123,15 +123,18 @@ class Timeline { ? 'at ${frame.member} ${frame.uri}:${frame.line}:${frame.column}' : null; - buffer.write('${event.name}'); + buffer.writeln('==================== Timeline Event ===================='); + buffer.writeln('Event: ${event.name}'); if (caller != null) { - buffer.write(' $caller'); + buffer.writeln('Caller: $caller'); } - if (event.screenshot != null) { - buffer.write('\nScreenshot: file://${event.screenshot!.file.path}'); + buffer.writeln('Screenshot: file://${event.screenshot!.file.path}'); } -// ignore: avoid_print + buffer.writeln('Timestamp: ${event.timestamp.toIso8601String()}'); + buffer.writeln('========================================================'); + + // ignore: avoid_print print(buffer); } } diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index 8e52a76d..3cf7a71a 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -180,32 +180,36 @@ void main() async { tempDir.deleteSync(recursive: true); final stdout = stdoutBuffer.toString(); - final timeline = stdout.split('\n'); + expect(timeline.first, 'Timeline'); expect( - timeline[1].startsWith( - 'Tap Icon Widget with icon: "IconData(U+0E047)" at main. file:///', - ), + timeline[1], + '==================== Timeline Event ====================', + ); + expect( + timeline[2], + 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', + ); + expect( + timeline[3].startsWith('Caller: at main. file:///'), isTrue, ); expect( - timeline[2].startsWith( + timeline[4].startsWith( 'Screenshot: file:///', ), isTrue, ); expect( - timeline[3].startsWith( - 'Tap Icon Widget with icon: "IconData(U+0E516)" at main. file:///', + timeline[5].startsWith( + 'Timestamp:', ), isTrue, ); expect( - timeline[4].startsWith( - 'Screenshot: file:///', - ), - isTrue, + timeline[6], + '========================================================', ); }); }); From 8309bfcf00f4c845f5645506f16aeee7d346d0c3 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 29 May 2024 02:40:31 +0200 Subject: [PATCH 035/119] Extend tests --- test/timeline/timeline_test.dart | 42 ++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index 3cf7a71a..f6cf7ea2 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -8,8 +8,6 @@ import 'package:test_process/test_process.dart'; import 'timeline_test_widget.dart'; -Iterable _screenshotMessageMatcher(String outPut) => - RegExp('Screenshot: file:').allMatches(outPut); final _addButtonSelector = spotIcon(Icons.add); final _subtractButtonSelector = spotIcon(Icons.remove); final _clearButtonSelector = spotIcon(Icons.clear); @@ -29,13 +27,16 @@ void main() { recordLiveTimeline(); }); expect(output, contains('馃敶 - Now recording live timeline')); - expect(output, contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')); expect( output, - contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), + contains('Event: Tap ${_addButtonSelector.toStringBreadcrumb()}'), + ); + expect( + output, + contains('Event: Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), ); expect(output, contains('馃敶 - Already recording live timeline')); - expect(_screenshotMessageMatcher(output).length, 2); + _testTimeLineContent(output); }); testWidgets('Start with Timeline Mode off', (tester) async { final output = await _captureConsoleOutput(() async { @@ -58,7 +59,7 @@ void main() { output, isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), ); - expect(_screenshotMessageMatcher(output).length, 0); + _testTimeLineContent(output, eventCount: 0); }); testWidgets('Turn timeline mode off during test', (tester) async { final output = await _captureConsoleOutput(() async { @@ -91,7 +92,7 @@ void main() { output, isNot(contains('Tap ${_clearButtonSelector.toStringBreadcrumb()}')), ); - expect(_screenshotMessageMatcher(output).length, 2); + _testTimeLineContent(output); expect(output, contains('鈴革笌 - Timeline recording is off')); }); @@ -118,8 +119,8 @@ void main() { output, isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), ); - expect(_screenshotMessageMatcher(output).length, 0); expect(output, contains('馃敶 - Already recording error output timeline')); + _testTimeLineContent(output, eventCount: 0); }); test('OnError timeline - with error, prints timeline', () async { @@ -215,6 +216,31 @@ void main() async { }); } +void _testTimeLineContent(String output, {int eventCount = 2}) { + expect( + RegExp('==================== Timeline Event ====================') + .allMatches(output) + .length, + eventCount, + ); + expect( + RegExp('Event: Tap Icon Widget with icon:').allMatches(output).length, + eventCount, + ); + expect( + RegExp('Caller: at main.. file:///').allMatches(output).length, + eventCount, + ); + expect( + RegExp('Screenshot: file:').allMatches(output).length, + eventCount, + ); + expect( + RegExp('Timestamp: ').allMatches(output).length, + eventCount, + ); +} + Future _captureConsoleOutput( Future Function() testFunction, ) async { From a559c6834c7ebfb4546c80a491b1a5eedd76e5b8 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 29 May 2024 02:53:45 +0200 Subject: [PATCH 036/119] Adjust documentation of takeScreenshotWithCrosshair --- lib/src/screenshot/screenshot.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/src/screenshot/screenshot.dart b/lib/src/screenshot/screenshot.dart index b6c3911f..ef587765 100644 --- a/lib/src/screenshot/screenshot.dart +++ b/lib/src/screenshot/screenshot.dart @@ -66,9 +66,7 @@ Future takeScreenshot({ /// - [snapshot]: A snapshot of the widget to capture. /// - [selector]: A selector to determine the widget to capture. /// - [name]: The name of the screenshot file. -/// - [tapPosition]: The position where the tap marker should be placed. -/// -/// The screenshot will have a crosshair painted at the [hitPosition]. +/// - [centerPosition]: The position where the tap marker should be placed. /// /// Returns a [Screenshot] object containing the file and initiator frame. Future takeScreenshotWithCrosshair({ From d6f78d27016975776ee9a9ba31c08440b350d452 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 29 May 2024 02:53:57 +0200 Subject: [PATCH 037/119] Make args of test named --- test/timeline/timeline_test.dart | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index f6cf7ea2..e3d8c45e 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -36,7 +36,7 @@ void main() { contains('Event: Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), ); expect(output, contains('馃敶 - Already recording live timeline')); - _testTimeLineContent(output); + _testTimeLineContent(output: output, eventCount: 2); }); testWidgets('Start with Timeline Mode off', (tester) async { final output = await _captureConsoleOutput(() async { @@ -59,7 +59,7 @@ void main() { output, isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), ); - _testTimeLineContent(output, eventCount: 0); + _testTimeLineContent(output: output, eventCount: 0); }); testWidgets('Turn timeline mode off during test', (tester) async { final output = await _captureConsoleOutput(() async { @@ -92,7 +92,7 @@ void main() { output, isNot(contains('Tap ${_clearButtonSelector.toStringBreadcrumb()}')), ); - _testTimeLineContent(output); + _testTimeLineContent(output: output, eventCount: 2); expect(output, contains('鈴革笌 - Timeline recording is off')); }); @@ -120,7 +120,7 @@ void main() { isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), ); expect(output, contains('馃敶 - Already recording error output timeline')); - _testTimeLineContent(output, eventCount: 0); + _testTimeLineContent(output: output, eventCount: 0); }); test('OnError timeline - with error, prints timeline', () async { @@ -216,7 +216,10 @@ void main() async { }); } -void _testTimeLineContent(String output, {int eventCount = 2}) { +void _testTimeLineContent({ + required String output, + required int eventCount, +}) { expect( RegExp('==================== Timeline Event ====================') .allMatches(output) From 99ff35a4d52ce4e68c4bd2c862aae836c4ff18c7 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Tue, 11 Jun 2024 15:33:44 +0200 Subject: [PATCH 038/119] Add preliminary html creation --- lib/src/timeline/timeline.dart | 217 +++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index 233453d6..89692763 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -1,4 +1,7 @@ // ignore_for_file: depend_on_referenced_packages +import 'dart:io'; + +import 'package:nanoid2/nanoid2.dart'; import 'package:spot/src/screenshot/screenshot.dart'; import 'package:spot/src/spot/tree_snapshot.dart'; //ignore: implementation_imports @@ -6,6 +9,8 @@ import 'package:test_api/src/backend/invoker.dart'; //ignore: implementation_imports import 'package:test_api/src/backend/live_test.dart'; +import 'package:path/path.dart' as path; + final Map _timelines = {}; /// Records the timeline and prints events as they happen. @@ -114,6 +119,8 @@ class Timeline { for (final event in _events) { _printEvent(event); } + + createTimelineHtmlFile(_events); } void _printEvent(TimelineEvent event) { @@ -196,3 +203,213 @@ enum TimelineMode { /// The timeline is not recording. off, } + + + + +/// Creates + + +void createTimelineHtmlFile(List events) { + final htmlBuffer = StringBuffer(); + + htmlBuffer.writeln(''); + htmlBuffer.writeln(''); + htmlBuffer.writeln('Timeline Events'); + htmlBuffer.writeln(''); + htmlBuffer.writeln(''); + htmlBuffer.writeln(''); + htmlBuffer.writeln(''); + htmlBuffer.writeln(''); + htmlBuffer.writeln('
'); + htmlBuffer.writeln(''); + htmlBuffer.writeln('

Timeline Events

'); + htmlBuffer.writeln('
'); + + for (int i = 0; i < events.length; i++) { + final event = events[i]; + final caller = event.initiator != null + ? 'at ${event.initiator!.member} ${event.initiator!.uri}:${event.initiator!.line}:${event.initiator!.column}' + : 'N/A'; + htmlBuffer.writeln('

#${i + 1}

'); + htmlBuffer.writeln('
'); + if (event.screenshot != null) { + final screenshotPath = event.screenshot!.file.path; + htmlBuffer.writeln('Screenshot'); + } + htmlBuffer.writeln('
'); + htmlBuffer.writeln('

Name: ${event.name ?? "Unnamed Event"}

'); + htmlBuffer.writeln('

Timestamp: ${event.timestamp.toIso8601String()}

'); + htmlBuffer.writeln('

Caller: $caller

'); + htmlBuffer.writeln('
'); + htmlBuffer.writeln('
'); + } + + htmlBuffer.writeln(''); + + htmlBuffer.writeln(''); + htmlBuffer.writeln(''); + + final tempDir = Directory.systemTemp.createTempSync(); + final htmlFile = File(path.join(tempDir.path, 'timeline_events.html')); + htmlFile.writeAsStringSync(htmlBuffer.toString()); + + // Printing the file URL to console + print('HTML file created: file://${htmlFile.path}'); +} + + +// htmlBuffer.writeln(''); +// htmlBuffer.writeln(''); +// +// final spotTempDir = Directory.systemTemp.createTempSync(); +// if (!spotTempDir.existsSync()) { +// spotTempDir.createSync(); +// } +// // always append a unique id to avoid name collisions +// final uniqueId = nanoid(length: 5); +// +// final htmlFile = File(path.join(spotTempDir.path, 'timeline_events_$uniqueId.html')); \ No newline at end of file From 41aa4924c9cb08739a2c01e4b33fa89e0412c709 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Tue, 11 Jun 2024 16:22:38 +0200 Subject: [PATCH 039/119] Adjust arrows --- lib/src/timeline/timeline.dart | 76 ++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 27 deletions(-) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index 89692763..4716b921 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -216,17 +216,20 @@ void createTimelineHtmlFile(List events) { htmlBuffer.writeln(''); htmlBuffer.writeln(''); htmlBuffer.writeln('Timeline Events'); - htmlBuffer.writeln(''); + htmlBuffer.writeln( + '',); htmlBuffer.writeln(''); htmlBuffer.writeln(''); htmlBuffer.writeln(''); htmlBuffer.writeln(''); From fb2ab579db2237beeab2e81e4030350ec4ad0542 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 12 Jun 2024 16:26:38 +0200 Subject: [PATCH 043/119] Extract test name with hierarchy --- lib/src/timeline/timeline.dart | 56 ++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index 06ebc7a3..d2c6da74 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -13,6 +13,62 @@ import 'package:test_api/src/backend/live_test.dart'; final Map _timelines = {}; +/// Returns the test name including the group hierarchy. +String testNameWithHierarchy() { + final test = Invoker.current?.liveTest; + if (test == null) { + return 'No test found'; + } + + // Group names are concatenated with the name of the previous group + final rawGroupNames = Invoker.current?.liveTest.groups + .map((group) { + if (group.name.isEmpty) { + return null; + } + return group.name; + }) + .whereNotNull() + .toList() ?? []; + + List removeRedundantParts(List inputList) { + if (inputList.length < 2) { + return inputList; + } + + final List outputList = []; + for (int i = 0; i < inputList.length - 1; i++) { + outputList.add(inputList[i]); + } + + String lastElement = inputList.last; + final String previousElement = inputList[inputList.length - 2]; + + // Remove the part of the last element that is included in the previous one + if (lastElement.startsWith(previousElement)) { + lastElement = lastElement.substring(previousElement.length).trim(); + } + + if (lastElement.isNotEmpty) { + outputList.add(lastElement); + } + + return outputList; + } + + final cleanedGroups = removeRedundantParts(rawGroupNames); + if (cleanedGroups.isNotEmpty) { + final joinedGroups = cleanedGroups.join(' '); + + final List fullNameParts = [joinedGroups, test.test.name]; + final String finalTestName = removeRedundantParts(fullNameParts).last; + final String groupHierarchy = cleanedGroups.join(' => '); + return '$finalTestName in group(s): $groupHierarchy'; + } else { + return test.test.name; + } +} + /// Records the timeline and prints events as they happen. void recordLiveTimeline() { final timeline = currentTimeline(); From 715d720df005ff995a9e77d237d67e08dc8ea69b Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 12 Jun 2024 16:40:27 +0200 Subject: [PATCH 044/119] Add test info in html --- lib/src/timeline/timeline.dart | 450 +++++++++++++++++---------------- 1 file changed, 230 insertions(+), 220 deletions(-) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index d2c6da74..9f9fce1a 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -1,6 +1,7 @@ // ignore_for_file: depend_on_referenced_packages import 'dart:io'; +import 'package:collection/collection.dart'; import 'package:nanoid2/nanoid2.dart'; import 'package:path/path.dart' as path; import 'package:spot/src/screenshot/screenshot.dart'; @@ -176,8 +177,236 @@ class Timeline { for (final event in _events) { _printEvent(event); } + _createTimelineHtmlFile(); + } + + void _createTimelineHtmlFile() { + final htmlBuffer = StringBuffer(); + + htmlBuffer.writeln(''); + htmlBuffer.writeln(''); + htmlBuffer.writeln('Timeline Events'); + htmlBuffer.writeln(''); + htmlBuffer.writeln(''); + htmlBuffer.writeln(''); + htmlBuffer.writeln(''); + htmlBuffer.writeln(''); + htmlBuffer.writeln('
'); + htmlBuffer.writeln(''); + htmlBuffer.writeln('

Timeline

'); + htmlBuffer.writeln('
'); - createTimelineHtmlFile(_events); + final nameWithHierarchy = testNameWithHierarchy(); + + htmlBuffer.writeln('

General

'); + htmlBuffer.writeln('

Name: $nameWithHierarchy

'); + + if(_events.isNotEmpty){ + htmlBuffer.writeln('

Events

'); + } + for (int i = 0; i < _events.length; i++) { + final event = _events[i]; + final caller = event.initiator != null + ? 'at ${event.initiator!.member} ${event.initiator!.uri}:${event.initiator!.line}:${event.initiator!.column}' + : 'N/A'; + final type = event.eventType != null ? event.eventType!.label : "Unknown event type"; + htmlBuffer.writeln('

#${i + 1}

'); + htmlBuffer.writeln('
'); + if (event.screenshot != null) { + final screenshotPath = event.screenshot!.file.path; + htmlBuffer.writeln('Screenshot'); + } + htmlBuffer.writeln('
'); + htmlBuffer.writeln('

Name: ${event.name ?? "Unnamed Event"}

'); + htmlBuffer.writeln('

Event Type: $type

'); + htmlBuffer.writeln('

Timestamp: ${event.timestamp.toIso8601String()}

'); + htmlBuffer.writeln('

Caller: $caller

'); + htmlBuffer.writeln('
'); + htmlBuffer.writeln('
'); + } + + htmlBuffer.writeln(''); + htmlBuffer.writeln(''); + htmlBuffer.writeln(''); + + final spotTempDir = Directory.systemTemp.createTempSync(); + if (!spotTempDir.existsSync()) { + spotTempDir.createSync(); + } + // always append a unique id to avoid name collisions + final uniqueId = nanoid(length: 5); + final htmlFile = File(path.join(spotTempDir.path, 'timeline_events_$uniqueId.html')); + htmlFile.writeAsStringSync(htmlBuffer.toString()); + + //ignore: avoid_print + print('View time line here: file://${htmlFile.path}'); } void _printEvent(TimelineEvent event) { @@ -278,222 +507,3 @@ enum TimelineMode { /// The timeline is not recording. off, } - - - - -/// Creates - - -void createTimelineHtmlFile(List events) { - final htmlBuffer = StringBuffer(); - - htmlBuffer.writeln(''); - htmlBuffer.writeln(''); - htmlBuffer.writeln('Timeline Events'); - htmlBuffer.writeln(''); - htmlBuffer.writeln(''); - htmlBuffer.writeln(''); - htmlBuffer.writeln(''); - htmlBuffer.writeln(''); - htmlBuffer.writeln('
'); - htmlBuffer.writeln(''); - htmlBuffer.writeln('

Timeline Events

'); - htmlBuffer.writeln('
'); - - for (int i = 0; i < events.length; i++) { - final event = events[i]; - final caller = event.initiator != null - ? 'at ${event.initiator!.member} ${event.initiator!.uri}:${event.initiator!.line}:${event.initiator!.column}' - : 'N/A'; - final type = event.eventType != null ? event.eventType!.label : "Unknown event type"; - htmlBuffer.writeln('

#${i + 1}

'); - htmlBuffer.writeln('
'); - if (event.screenshot != null) { - final screenshotPath = event.screenshot!.file.path; - htmlBuffer.writeln('Screenshot'); - } - htmlBuffer.writeln('
'); - htmlBuffer.writeln('

Name: ${event.name ?? "Unnamed Event"}

'); - htmlBuffer.writeln('

Event Type: $type

'); - htmlBuffer.writeln('

Timestamp: ${event.timestamp.toIso8601String()}

'); - htmlBuffer.writeln('

Caller: $caller

'); - htmlBuffer.writeln('
'); - htmlBuffer.writeln('
'); - } - - htmlBuffer.writeln(''); - htmlBuffer.writeln(''); - htmlBuffer.writeln(''); - - final spotTempDir = Directory.systemTemp.createTempSync(); - if (!spotTempDir.existsSync()) { - spotTempDir.createSync(); -} - // always append a unique id to avoid name collisions - final uniqueId = nanoid(length: 5); - final htmlFile = File(path.join(spotTempDir.path, 'timeline_events_$uniqueId.html')); - htmlFile.writeAsStringSync(htmlBuffer.toString()); - - //ignore: avoid_print - print('View time line here: file://${htmlFile.path}'); -} From b6f6dde794d5edcfa9b153aa1fb48d089dd1d849 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 12 Jun 2024 18:54:14 +0200 Subject: [PATCH 045/119] Refactor timeline html generation --- lib/src/timeline/timeline.dart | 461 +++++++++++++++++---------------- 1 file changed, 242 insertions(+), 219 deletions(-) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index 9f9fce1a..20a08728 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -177,225 +177,10 @@ class Timeline { for (final event in _events) { _printEvent(event); } - _createTimelineHtmlFile(); + _printHTML(); } - void _createTimelineHtmlFile() { - final htmlBuffer = StringBuffer(); - - htmlBuffer.writeln(''); - htmlBuffer.writeln(''); - htmlBuffer.writeln('Timeline Events'); - htmlBuffer.writeln(''); - htmlBuffer.writeln(''); - htmlBuffer.writeln(''); - htmlBuffer.writeln(''); - htmlBuffer.writeln(''); - htmlBuffer.writeln('
'); - htmlBuffer.writeln(''); - htmlBuffer.writeln('

Timeline

'); - htmlBuffer.writeln('
'); - - final nameWithHierarchy = testNameWithHierarchy(); - - htmlBuffer.writeln('

General

'); - htmlBuffer.writeln('

Name: $nameWithHierarchy

'); - - if(_events.isNotEmpty){ - htmlBuffer.writeln('

Events

'); - } - for (int i = 0; i < _events.length; i++) { - final event = _events[i]; - final caller = event.initiator != null - ? 'at ${event.initiator!.member} ${event.initiator!.uri}:${event.initiator!.line}:${event.initiator!.column}' - : 'N/A'; - final type = event.eventType != null ? event.eventType!.label : "Unknown event type"; - htmlBuffer.writeln('

#${i + 1}

'); - htmlBuffer.writeln('
'); - if (event.screenshot != null) { - final screenshotPath = event.screenshot!.file.path; - htmlBuffer.writeln('Screenshot'); - } - htmlBuffer.writeln('
'); - htmlBuffer.writeln('

Name: ${event.name ?? "Unnamed Event"}

'); - htmlBuffer.writeln('

Event Type: $type

'); - htmlBuffer.writeln('

Timestamp: ${event.timestamp.toIso8601String()}

'); - htmlBuffer.writeln('

Caller: $caller

'); - htmlBuffer.writeln('
'); - htmlBuffer.writeln('
'); - } - - htmlBuffer.writeln(''); - htmlBuffer.writeln(''); - htmlBuffer.writeln(''); - + void _printHTML() { final spotTempDir = Directory.systemTemp.createTempSync(); if (!spotTempDir.existsSync()) { spotTempDir.createSync(); @@ -403,8 +188,8 @@ class Timeline { // always append a unique id to avoid name collisions final uniqueId = nanoid(length: 5); final htmlFile = File(path.join(spotTempDir.path, 'timeline_events_$uniqueId.html')); - htmlFile.writeAsStringSync(htmlBuffer.toString()); - + final content = _timelineAsHTML(); + htmlFile.writeAsStringSync(content); //ignore: avoid_print print('View time line here: file://${htmlFile.path}'); } @@ -430,6 +215,244 @@ class Timeline { // ignore: avoid_print print(buffer); } + + /// Returns the events in the timeline as an HTML string. + String _timelineAsHTML(){ + final htmlBuffer = StringBuffer(); + final nameWithHierarchy = testNameWithHierarchy(); + const style = ''' + +'''; + final script = ''' + +'''; + final events = (){ + final eventBuffer = StringBuffer(); + for (final event in _events) { + final part = () { + final index = _events.indexOf(event); + final caller = event.initiator != null + ? 'at ${event.initiator!.member} ${event.initiator!.uri}:${event.initiator!.line}:${event.initiator!.column}' + : 'N/A'; + final type = event.eventType != null ? event.eventType!.label : "Unknown event type"; + final screenshot = event.screenshot != null ? 'Screenshot' : ''; + + return ''' +

#${index + 1}

+
+ $screenshot +
+

Name: ${event.name ?? "Unnamed Event"}

+

Event Type: $type

+

Timestamp: ${event.timestamp.toIso8601String()}

+

Caller: $caller

+
+
+'''; + }(); + eventBuffer.write(part); + } + return eventBuffer.toString(); + }(); + + htmlBuffer.writeln(''); + htmlBuffer.writeln(''); + htmlBuffer.writeln('Timeline Events'); + htmlBuffer.writeln(''); + htmlBuffer.write(style); + htmlBuffer.write(script); + htmlBuffer.writeln(''); + htmlBuffer.writeln(''); + htmlBuffer.writeln('
'); + htmlBuffer.writeln(''); + htmlBuffer.writeln('

Timeline

'); + htmlBuffer.writeln('
'); + + htmlBuffer.writeln('

General

'); + + htmlBuffer.writeln('

Name: $nameWithHierarchy

'); + if(_events.isNotEmpty){ + htmlBuffer.writeln('

Events

'); + } + + htmlBuffer.write(events); + + htmlBuffer.writeln(''); + htmlBuffer.writeln(''); + htmlBuffer.writeln(''); + + return htmlBuffer.toString(); + } } /// The type of event that occurred during a test. From 2d6b3f92593041b84e5310f4f1a17aee9fc83d1f Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Thu, 13 Jun 2024 02:37:22 +0200 Subject: [PATCH 046/119] Add re-run test button --- lib/src/timeline/timeline.dart | 198 +++++++++++++++++++++++++++------ 1 file changed, 166 insertions(+), 32 deletions(-) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index 20a08728..a3992acb 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -222,6 +222,7 @@ class Timeline { final nameWithHierarchy = testNameWithHierarchy(); const style = ''' '''; final script = ''' @@ -344,47 +419,104 @@ const events = [ return '{src: "file://${event.screenshot!.file.path}", title: "${event.eventType?.label ?? "Event ${_events.indexOf(event) + 1}"}"},'; },).join('\n ')} ]; +/** + * Copies the test command to the clipboard and shows a SnackBar with the result. + */ +function copyTestCommandToClipboard() { + var testName = "${Invoker.current?.liveTest.test.name}"; + console.log("Re-running test: " + testName); + var command = `flutter test --plain-name="\${testName}"`; + navigator.clipboard.writeText(command).then(function() { + showSnackbar("Test command copied to clipboard"); + }, function(err) { + showSnackbar("Failed to copy test command"); + }); +} + +/** + * Displays a SnackBar with a specified message. + * @param {string} message - The message to display in the SnackBar. + */ +function showSnackbar(message) { + var snackbar = document.getElementById("snackbar"); + snackbar.textContent = message; + snackbar.className = "snackbar show"; + setTimeout(function() { + snackbar.className = snackbar.className.replace("show", ""); + }, 3000); +} + +/** + * Opens a modal to display an image and its caption. + * @param {number} index - The index of the image to display. + */ function openModal(index) { - currentIndex = index; - var modal = document.getElementById("myModal"); - var modalImg = document.getElementById("img01"); - var captionText = document.getElementById("captionText"); - modal.style.display = "block"; - modalImg.src = events[index].src; - captionText.innerHTML = events[index].title; -} + currentIndex = index; + var modal = document.getElementById("myModal"); + var modalImg = document.getElementById("img01"); + var captionText = document.getElementById("captionText"); + modal.style.display = "block"; + modalImg.src = events[index].src; + captionText.innerHTML = events[index].title; +} + +/** + * Closes the modal. + */ function closeModal() { - var modal = document.getElementById("myModal"); - modal.style.display = "none"; + var modal = document.getElementById("myModal"); + modal.style.display = "none"; } + +/** + * Shows the previous image in the modal. + */ function showPrev() { - currentIndex = (currentIndex + events.length - 1) % events.length; - updateModal(); + currentIndex = (currentIndex + events.length - 1) % events.length; + updateModal(); } + +/** + * Shows the next image in the modal. + */ function showNext() { - currentIndex = (currentIndex + 1) % events.length; - updateModal(); + currentIndex = (currentIndex + 1) % events.length; + updateModal(); } + +/** + * Updates the modal content to display the current image and caption. + */ function updateModal() { - var modalImg = document.getElementById("img01"); - var captionText = document.getElementById("captionText"); - modalImg.src = events[currentIndex].src; - captionText.innerHTML = events[currentIndex].title; + var modalImg = document.getElementById("img01"); + var captionText = document.getElementById("captionText"); + modalImg.src = events[currentIndex].src; + captionText.innerHTML = events[currentIndex].title; } + +/** + * Closes the modal when clicking outside of it. + * @param {Event} event - The event triggered by the click. + */ window.onclick = function(event) { - var modal = document.getElementById("myModal"); - if (event.target == modal) { - modal.style.display = "none"; - } + var modal = document.getElementById("myModal"); + if (event.target == modal) { + modal.style.display = "none"; + } } + +/** + * Adds keyboard navigation for the modal. + * @param {Event} event - The event triggered by the key press. + */ window.addEventListener("keydown", function(event) { - if (event.key === "ArrowLeft") { - showPrev(); - } else if (event.key === "ArrowRight") { - showNext(); - } else if (event.key === "Escape") { - closeModal(); - } + if (event.key === "ArrowLeft") { + showPrev(); + } else if (event.key === "ArrowRight") { + showNext(); + } else if (event.key === "Escape") { + closeModal(); + } }); '''; @@ -416,7 +548,6 @@ window.addEventListener("keydown", function(event) { } return eventBuffer.toString(); }(); - htmlBuffer.writeln(''); htmlBuffer.writeln(''); htmlBuffer.writeln('Timeline Events'); @@ -430,9 +561,12 @@ window.addEventListener("keydown", function(event) { htmlBuffer.writeln('

Timeline

'); htmlBuffer.writeln(''); - htmlBuffer.writeln('

General

'); + htmlBuffer.writeln('

Info

'); + + htmlBuffer.writeln('

Test: $nameWithHierarchy

'); + htmlBuffer.writeln(''); + htmlBuffer.writeln('
'); - htmlBuffer.writeln('

Name: $nameWithHierarchy

'); if(_events.isNotEmpty){ htmlBuffer.writeln('

Events

'); } From a3d27fa195980f2432d5d4f4d343ab59bced5244 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Thu, 13 Jun 2024 04:07:08 +0200 Subject: [PATCH 047/119] Add script js --- lib/src/timeline/script.js | 104 +++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 lib/src/timeline/script.js diff --git a/lib/src/timeline/script.js b/lib/src/timeline/script.js new file mode 100644 index 00000000..c79c490a --- /dev/null +++ b/lib/src/timeline/script.js @@ -0,0 +1,104 @@ +let currentIndex = 0; +const events = [ + {{events}} +]; + +/** + * Copies the test command to the clipboard and shows a SnackBar with the result. + */ +function copyTestCommandToClipboard() { + var testName = "Test Name"; // Replace with appropriate value + console.log("Re-running test: " + testName); + var command = `flutter test --plain-name="\${testName}"`; + navigator.clipboard.writeText(command).then(function() { + showSnackbar("Test command copied to clipboard"); + }, function(err) { + showSnackbar("Failed to copy test command"); + }); +} + +/** + * Displays a SnackBar with a specified message. + * @param {string} message - The message to display in the SnackBar. + */ +function showSnackbar(message) { + var snackbar = document.getElementById("snackbar"); + snackbar.textContent = message; + snackbar.className = "snackbar show"; + setTimeout(function() { + snackbar.className = snackbar.className.replace("show", ""); + }, 3000); +} + +/** + * Opens a modal to display an image and its caption. + * @param {number} index - The index of the image to display. + */ +function openModal(index) { + currentIndex = index; + var modal = document.getElementById("myModal"); + var modalImg = document.getElementById("img01"); + var captionText = document.getElementById("captionText"); + modal.style.display = "block"; + modalImg.src = events[index].src; + captionText.innerHTML = events[index].title; +} + +/** + * Closes the modal. + */ +function closeModal() { + var modal = document.getElementById("myModal"); + modal.style.display = "none"; +} + +/** + * Shows the previous image in the modal. + */ +function showPrev() { + currentIndex = (currentIndex + events.length - 1) % events.length; + updateModal(); +} + +/** + * Shows the next image in the modal. + */ +function showNext() { + currentIndex = (currentIndex + 1) % events.length; + updateModal(); +} + +/** + * Updates the modal content to display the current image and caption. + */ +function updateModal() { + var modalImg = document.getElementById("img01"); + var captionText = document.getElementById("captionText"); + modalImg.src = events[currentIndex].src; + captionText.innerHTML = events[currentIndex].title; +} + +/** + * Closes the modal when clicking outside of it. + * @param {Event} event - The event triggered by the click. + */ +window.onclick = function(event) { + var modal = document.getElementById("myModal"); + if (event.target == modal) { + modal.style.display = "none"; + } +} + +/** + * Adds keyboard navigation for the modal. + * @param {Event} event - The event triggered by the key press. + */ +window.addEventListener("keydown", function(event) { + if (event.key === "ArrowLeft") { + showPrev(); + } else if (event.key === "ArrowRight") { + showNext(); + } else if (event.key === "Escape") { + closeModal(); + } +}); \ No newline at end of file From 9d9b67ef5a2e625134685b91cf29ad9fadc7f208 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Thu, 13 Jun 2024 04:07:17 +0200 Subject: [PATCH 048/119] Add styles css --- lib/src/timeline/styles.css | 183 ++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 lib/src/timeline/styles.css diff --git a/lib/src/timeline/styles.css b/lib/src/timeline/styles.css new file mode 100644 index 00000000..e40d1f12 --- /dev/null +++ b/lib/src/timeline/styles.css @@ -0,0 +1,183 @@ +body { + box-sizing: border-box; + background-color: #F0FCFF; + color: #4a4a4a; + overflow-wrap: anywhere; + font-family: "Google Sans Text","Google Sans","Roboto",sans-serif; + -webkit-font-smoothing: antialiased; +} + +.header { + display: flex; + align-items: center; + padding: 10px; +} + +h1 { + color: #4a4a4a; + overflow-wrap: anywhere; + -webkit-font-smoothing: antialiased; + font-variant-ligatures: none; + font-feature-settings: "liga" 0; + box-sizing: border-box; + font-weight: 400; + font-family: "Google Sans Display","Google Sans","Roboto",sans-serif; + margin: 0; + font-size: 36px; + padding-left: 10px; +} + +.event { + display: flex; + border: 2px solid #557783; + margin: 10px; + padding: 10px; + background-color: #ffffff; +} + +.event-details { + margin-left: 20px; +} + +.thumbnail { + height: 150px; + cursor: pointer; + border: 1px solid #557783; + object-fit: contain; +} + +.modal { + display: none; + position: fixed; + z-index: 1; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0,0,0,0.9); +} + +.modal-content { + margin: auto; + display: block; + max-width: 80%; + height: auto; +} + +.close { + position: absolute; + top: 15px; + right: 35px; + color: #f1f1f1; + font-size: 40px; + font-weight: bold; +} + +.close:hover, .close:focus { + color: #C97B2D; + text-decoration: none; + cursor: pointer; +} + +.nav { + padding: 10px; + color: white; + font-weight: bold; + font-size: 30px; + cursor: pointer; + user-select: none; +} + +.nav:hover { + color: #C97B2D; +} + +.nav-left { + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); +} + +.nav-right { + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); +} + +#caption { + text-align: center; + color: #ccc; + padding: 10px 0; + height: 150px; +} + +.horizontal-spacer { + border-bottom: 1px solid #C97B2D; + padding-top: 25px; +} + +.horizontal-spacer h2 { + margin: 0; + padding: 0; +} + +.button-spot { + background-color: #557783; + color: white; + border: none; + border-radius: 5px; + padding: 10px 20px; + font-family: 'Arial', sans-serif; + font-size: 16px; + cursor: pointer; + transition: background-color 0.3s ease, color 0.3s ease; +} + +.button-spot:hover { + background-color: #C97B2D; +} + +.snackbar { + display: none; + min-width: 250px; + margin-left: -125px; + background-color: #333; + color: #fff; + text-align: center; + border-radius: 2px; + padding: 16px; + position: fixed; + z-index: 1; + left: 50%; + bottom: 30px; + font-size: 17px; +} + +.snackbar.show { + display: block; + -webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s; + animation: fadein 0.5s, fadeout 0.5s 2.5s; +} + +@-webkit-keyframes fadein { + from {bottom: 0; opacity: 0;} + to {bottom: 30px; opacity: 1;} +} + +@keyframes fadein { + from {bottom: 0; opacity: 0;} + to {bottom: 30px; opacity: 1;} +} + +@-webkit-keyframes fadeout { + from {bottom: 30px; opacity: 1;} + to {bottom: 0; opacity: 0;} +} + +@keyframes fadeout { + from {bottom: 30px; opacity: 1;} + to {bottom: 0; opacity: 0;} +} \ No newline at end of file From a7598419c2d3da439e8c58b29ac24848fcd91283 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Thu, 13 Jun 2024 04:07:29 +0200 Subject: [PATCH 049/119] Use script and styles from File --- lib/src/timeline/timeline.dart | 342 +++------------------------------ 1 file changed, 29 insertions(+), 313 deletions(-) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index a3992acb..271cdfcf 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -1,6 +1,5 @@ // ignore_for_file: depend_on_referenced_packages import 'dart:io'; - import 'package:collection/collection.dart'; import 'package:nanoid2/nanoid2.dart'; import 'package:path/path.dart' as path; @@ -220,306 +219,45 @@ class Timeline { String _timelineAsHTML(){ final htmlBuffer = StringBuffer(); final nameWithHierarchy = testNameWithHierarchy(); - const style = ''' - -'''; - final script = ''' -'); -/** - * Closes the modal. - */ -function closeModal() { - var modal = document.getElementById("myModal"); - modal.style.display = "none"; -} + final css = File('$timelinePath/styles.css').readAsLinesSync(); + htmlBuffer.writeln(''); -/** - * Shows the previous image in the modal. - */ -function showPrev() { - currentIndex = (currentIndex + events.length - 1) % events.length; - updateModal(); -} + htmlBuffer.writeln(''); + htmlBuffer.writeln(''); + htmlBuffer.writeln('
'); + htmlBuffer.writeln(''); + htmlBuffer.writeln('

Timeline

'); + htmlBuffer.writeln('
'); -/** - * Shows the next image in the modal. - */ -function showNext() { - currentIndex = (currentIndex + 1) % events.length; - updateModal(); -} + htmlBuffer.writeln('

Info

'); -/** - * Updates the modal content to display the current image and caption. - */ -function updateModal() { - var modalImg = document.getElementById("img01"); - var captionText = document.getElementById("captionText"); - modalImg.src = events[currentIndex].src; - captionText.innerHTML = events[currentIndex].title; -} + htmlBuffer.writeln('

Test: $nameWithHierarchy

'); + htmlBuffer.writeln(''); + htmlBuffer.writeln('
'); -/** - * Closes the modal when clicking outside of it. - * @param {Event} event - The event triggered by the click. - */ -window.onclick = function(event) { - var modal = document.getElementById("myModal"); - if (event.target == modal) { - modal.style.display = "none"; + if(_events.isNotEmpty){ + htmlBuffer.writeln('

Events

'); } -} -/** - * Adds keyboard navigation for the modal. - * @param {Event} event - The event triggered by the key press. - */ -window.addEventListener("keydown", function(event) { - if (event.key === "ArrowLeft") { - showPrev(); - } else if (event.key === "ArrowRight") { - showNext(); - } else if (event.key === "Escape") { - closeModal(); - } -}); - -'''; final events = (){ final eventBuffer = StringBuffer(); for (final event in _events) { @@ -548,28 +286,6 @@ window.addEventListener("keydown", function(event) { } return eventBuffer.toString(); }(); - htmlBuffer.writeln(''); - htmlBuffer.writeln(''); - htmlBuffer.writeln('Timeline Events'); - htmlBuffer.writeln(''); - htmlBuffer.write(style); - htmlBuffer.write(script); - htmlBuffer.writeln(''); - htmlBuffer.writeln(''); - htmlBuffer.writeln('
'); - htmlBuffer.writeln(''); - htmlBuffer.writeln('

Timeline

'); - htmlBuffer.writeln('
'); - - htmlBuffer.writeln('

Info

'); - - htmlBuffer.writeln('

Test: $nameWithHierarchy

'); - htmlBuffer.writeln(''); - htmlBuffer.writeln('
'); - - if(_events.isNotEmpty){ - htmlBuffer.writeln('

Events

'); - } htmlBuffer.write(events); From 3db06ba1c9b596bdcf811183e233e18146be1d3b Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Thu, 13 Jun 2024 17:03:41 +0200 Subject: [PATCH 050/119] Make script.js and css.js dart files --- .../timeline/{script.js => script.js.dart} | 12 +- lib/src/timeline/styles.css | 183 ------------- lib/src/timeline/styles.css.dart | 246 ++++++++++++++++++ lib/src/timeline/timeline.dart | 13 +- 4 files changed, 260 insertions(+), 194 deletions(-) rename lib/src/timeline/{script.js => script.js.dart} (93%) delete mode 100644 lib/src/timeline/styles.css create mode 100644 lib/src/timeline/styles.css.dart diff --git a/lib/src/timeline/script.js b/lib/src/timeline/script.js.dart similarity index 93% rename from lib/src/timeline/script.js rename to lib/src/timeline/script.js.dart index c79c490a..446e299b 100644 --- a/lib/src/timeline/script.js +++ b/lib/src/timeline/script.js.dart @@ -1,3 +1,8 @@ + +// language=javascript +/// The script used in the HTML file that is generated for the timeline. +const String timelineJS = r''' + let currentIndex = 0; const events = [ {{events}} @@ -7,9 +12,7 @@ const events = [ * Copies the test command to the clipboard and shows a SnackBar with the result. */ function copyTestCommandToClipboard() { - var testName = "Test Name"; // Replace with appropriate value - console.log("Re-running test: " + testName); - var command = `flutter test --plain-name="\${testName}"`; + var command = `flutter test --plain-name="{testName}"`; navigator.clipboard.writeText(command).then(function() { showSnackbar("Test command copied to clipboard"); }, function(err) { @@ -101,4 +104,5 @@ window.addEventListener("keydown", function(event) { } else if (event.key === "Escape") { closeModal(); } -}); \ No newline at end of file +}); +'''; \ No newline at end of file diff --git a/lib/src/timeline/styles.css b/lib/src/timeline/styles.css deleted file mode 100644 index e40d1f12..00000000 --- a/lib/src/timeline/styles.css +++ /dev/null @@ -1,183 +0,0 @@ -body { - box-sizing: border-box; - background-color: #F0FCFF; - color: #4a4a4a; - overflow-wrap: anywhere; - font-family: "Google Sans Text","Google Sans","Roboto",sans-serif; - -webkit-font-smoothing: antialiased; -} - -.header { - display: flex; - align-items: center; - padding: 10px; -} - -h1 { - color: #4a4a4a; - overflow-wrap: anywhere; - -webkit-font-smoothing: antialiased; - font-variant-ligatures: none; - font-feature-settings: "liga" 0; - box-sizing: border-box; - font-weight: 400; - font-family: "Google Sans Display","Google Sans","Roboto",sans-serif; - margin: 0; - font-size: 36px; - padding-left: 10px; -} - -.event { - display: flex; - border: 2px solid #557783; - margin: 10px; - padding: 10px; - background-color: #ffffff; -} - -.event-details { - margin-left: 20px; -} - -.thumbnail { - height: 150px; - cursor: pointer; - border: 1px solid #557783; - object-fit: contain; -} - -.modal { - display: none; - position: fixed; - z-index: 1; - left: 0; - top: 0; - width: 100%; - height: 100%; - overflow: auto; - background-color: rgba(0,0,0,0.9); -} - -.modal-content { - margin: auto; - display: block; - max-width: 80%; - height: auto; -} - -.close { - position: absolute; - top: 15px; - right: 35px; - color: #f1f1f1; - font-size: 40px; - font-weight: bold; -} - -.close:hover, .close:focus { - color: #C97B2D; - text-decoration: none; - cursor: pointer; -} - -.nav { - padding: 10px; - color: white; - font-weight: bold; - font-size: 30px; - cursor: pointer; - user-select: none; -} - -.nav:hover { - color: #C97B2D; -} - -.nav-left { - position: absolute; - left: 0; - top: 50%; - transform: translateY(-50%); -} - -.nav-right { - position: absolute; - right: 0; - top: 50%; - transform: translateY(-50%); -} - -#caption { - text-align: center; - color: #ccc; - padding: 10px 0; - height: 150px; -} - -.horizontal-spacer { - border-bottom: 1px solid #C97B2D; - padding-top: 25px; -} - -.horizontal-spacer h2 { - margin: 0; - padding: 0; -} - -.button-spot { - background-color: #557783; - color: white; - border: none; - border-radius: 5px; - padding: 10px 20px; - font-family: 'Arial', sans-serif; - font-size: 16px; - cursor: pointer; - transition: background-color 0.3s ease, color 0.3s ease; -} - -.button-spot:hover { - background-color: #C97B2D; -} - -.snackbar { - display: none; - min-width: 250px; - margin-left: -125px; - background-color: #333; - color: #fff; - text-align: center; - border-radius: 2px; - padding: 16px; - position: fixed; - z-index: 1; - left: 50%; - bottom: 30px; - font-size: 17px; -} - -.snackbar.show { - display: block; - -webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s; - animation: fadein 0.5s, fadeout 0.5s 2.5s; -} - -@-webkit-keyframes fadein { - from {bottom: 0; opacity: 0;} - to {bottom: 30px; opacity: 1;} -} - -@keyframes fadein { - from {bottom: 0; opacity: 0;} - to {bottom: 30px; opacity: 1;} -} - -@-webkit-keyframes fadeout { - from {bottom: 30px; opacity: 1;} - to {bottom: 0; opacity: 0;} -} - -@keyframes fadeout { - from {bottom: 30px; opacity: 1;} - to {bottom: 0; opacity: 0;} -} \ No newline at end of file diff --git a/lib/src/timeline/styles.css.dart b/lib/src/timeline/styles.css.dart new file mode 100644 index 00000000..5971cae8 --- /dev/null +++ b/lib/src/timeline/styles.css.dart @@ -0,0 +1,246 @@ +/// The CSS used in the HTML file that is generated for the timeline. +const timelineCSS = ''' +:root { + --spot-grey: #4a4a4a; + --spot-green: #557783; + --spot-background: #F0FCFF; + --spot-orange: #C97B2D; + --event-box-height: 170px; + --thumbnail-height: 150px; + --font-family: "Google Sans Text","Google Sans","Roboto",sans-serif; + --font-color: var(--spot-green); + --h1-font-size: 36px; + --h1-padding-left: 10px; + --border-color: var(--spot-orange); + --border-width: 2px; + --event-margin: 10px; + --event-padding: 10px; + --event-details-margin-left: 20px; + --event-details-padding: 5px 0; + --thumbnail-border: 1px solid var(--spot-green); + --modal-background-color: rgba(0,0,0,0.9); + --close-color: white; + --close-font-size: 40px; + --close-hover-color: var(--spot-orange); + --nav-color: white; + --nav-background-color: transparent; + --nav-font-size: 30px; + --nav-hover-color: var(--spot-orange); + --caption-color: #ccc; + --caption-padding: 10px 0; + --horizontal-spacer-border-color: var(--spot-orange); + --horizontal-spacer-padding-top: 25px; + --button-background-color: var(--spot-green); + --button-color: white; + --button-hover-background-color: var(--spot-orange); + --button-padding: 10px 20px; + --button-font-size: 16px; + --button-border-radius: 5px; + --snackbar-background-color: var(--spot-orange); + --snackbar-color: white; + --snackbar-font-size: 17px; +} + +* { + font-family: var(--font-family); + background-color: var(--spot-background); + -webkit-font-smoothing: antialiased; +} + +body { + margin: 10; +} + +h1, h2, h3, p { + color: var(--font-color); +} + +h1 { + font-weight: 400; + font-size: var(--h1-font-size); + padding-left: var(--h1-padding-left); +} + +.header { + display: flex; + align-items: center; +} + +.event { + display: flex; + align-items: center; + border: var(--border-width) solid var(--border-color); + margin: var(--event-margin); + padding: var(--event-padding); + height: var(--event-box-height); +} + +.event-details { + display: flex; + flex-direction: column; + justify-content: space-between; + margin-left: var(--event-details-margin-left); + height: 100%; +} + +.event-details p { + margin: 0; + padding: var(--event-details-padding); + flex-grow: 1; +} + +.thumbnail { + height: var(--thumbnail-height); + cursor: pointer; + border: var(--thumbnail-border); + object-fit: contain; +} + +.modal { + display: none; + position: fixed; + z-index: 1; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: var(--modal-background-color); +} + +.modal img { +max-width: 800px; +} + +.modal span { + background-color: transparent; +} + +.modal-content { + margin: auto; + display: block; + max-width: 80%; + height: auto; + background-color: transparent; + border: none; +} + +.close { + position: absolute; + top: 15px; + right: 35px; + color: var(--close-color); + font-size: var(--close-font-size); + font-weight: bold; +} + +.close:hover, .close:focus { + color: var(--close-hover-color); + text-decoration: none; + cursor: pointer; +} + +.nav { + position: absolute; + top: 50%; + font-weight: bold; + font-size: var(--nav-font-size); + cursor: pointer; + user-select: none; + color: var(--nav-color); + background-color: var(--nav-background-color); + transform: translateY(-50%); + padding: 10px; +} + +.nav:hover { + color: var(--nav-hover-color); +} + +.nav-left { + left: 0; +} + +.nav-right { + right: 0; +} + +#caption { + text-align: center; + color: var(--caption-color); + background-color: transparent; + padding: var(--caption-padding); + height: 150px; +} + +#caption div { + background-color: transparent; +} + +.horizontal-spacer { + border-bottom: 1px solid var(--horizontal-spacer-border-color); + padding-top: var(--horizontal-spacer-padding-top); +} + +.horizontal-spacer h2 { + margin: 0; + padding: 0; +} + +.button-spot { + background-color: var(--button-background-color); + color: var(--button-color); + border: none; + border-radius: var(--button-border-radius); + padding: var(--button-padding); + font-size: var(--button-font-size); + cursor: pointer; + transition: background-color 0.3s ease, color 0.3s ease; +} + +.button-spot:hover { + background-color: var(--button-hover-background-color); +} + +.snackbar { + display: none; + min-width: 250px; + margin-left: -125px; + background-color: var(--snackbar-background-color); + color: var(--snackbar-color); + text-align: center; + border-radius: 2px; + padding: 16px; + position: fixed; + z-index: 1; + left: 50%; + bottom: 30px; + font-size: var(--snackbar-font-size); +} + +.snackbar.show { + display: block; + -webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s; + animation: fadein 0.5s, fadeout 0.5s 2.5s; +} + +@-webkit-keyframes fadein { + from {bottom: 0; opacity: 0;} + to {bottom: 30px; opacity: 1;} +} + +@keyframes fadein { + from {bottom: 0; opacity: 0;} + to {bottom: 30px; opacity: 1;} +} + +@-webkit-keyframes fadeout { + from {bottom: 30px; opacity: 1;} + to {bottom: 0; opacity: 0;} +} + +@keyframes fadeout { + from {bottom: 30px; opacity: 1;} + to {bottom: 0; opacity: 0;} +} +'''; \ No newline at end of file diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index 271cdfcf..cd1e0f17 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -5,6 +5,8 @@ import 'package:nanoid2/nanoid2.dart'; import 'package:path/path.dart' as path; import 'package:spot/src/screenshot/screenshot.dart'; import 'package:spot/src/spot/tree_snapshot.dart'; +import 'package:spot/src/timeline/script.js.dart'; +import 'package:spot/src/timeline/styles.css.dart'; //ignore: implementation_imports import 'package:test_api/src/backend/invoker.dart'; //ignore: implementation_imports @@ -225,20 +227,17 @@ class Timeline { htmlBuffer.writeln('Timeline Events'); htmlBuffer.writeln(''); - final timelinePath = '${Directory.current.path}/lib/src/timeline/'; - final String eventsForScript = _events.where((event) => event.screenshot != null).map((event) { return '{src: "file://${event.screenshot!.file.path}", title: "${event.eventType?.label ?? "Event ${_events.indexOf(event) + 1}"}"}'; }).join(',\n '); - final script = File('$timelinePath/script.js').readAsStringSync().replaceAll('{{events}}', eventsForScript); htmlBuffer.writeln(''); - final css = File('$timelinePath/styles.css').readAsLinesSync(); htmlBuffer.writeln(''); htmlBuffer.writeln(''); @@ -291,10 +290,10 @@ class Timeline { htmlBuffer.writeln(''); From d15ad98d1f84c43d100fb8e5a36c476ac4cf7b7c Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Thu, 13 Jun 2024 17:59:02 +0200 Subject: [PATCH 051/119] Add first html test --- test/timeline/timeline_test.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index 7c9db54f..ddcc3627 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -181,7 +181,7 @@ void main() async { tempDir.deleteSync(recursive: true); final stdout = stdoutBuffer.toString(); - final timeline = stdout.split('\n'); + final timeline = stdout.split('\n')..removeWhere((line) => line.isEmpty); expect(timeline.first, 'Timeline'); expect( timeline[1], @@ -211,6 +211,10 @@ void main() async { timeline[6], '========================================================', ); + expect( + timeline.last, + startsWith('View time line here:'), + ); }); }); } From 30c0b2d1964cfc7d2c153bc4f1e7c4dd3516b96a Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 14 Jun 2024 02:58:10 +0200 Subject: [PATCH 052/119] Fix format --- lib/src/timeline/script.js.dart | 4 ++-- lib/src/timeline/styles.css.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/timeline/script.js.dart b/lib/src/timeline/script.js.dart index 446e299b..6806b0a0 100644 --- a/lib/src/timeline/script.js.dart +++ b/lib/src/timeline/script.js.dart @@ -1,7 +1,7 @@ // language=javascript /// The script used in the HTML file that is generated for the timeline. -const String timelineJS = r''' +const String timelineJS = ''' let currentIndex = 0; const events = [ @@ -105,4 +105,4 @@ window.addEventListener("keydown", function(event) { closeModal(); } }); -'''; \ No newline at end of file +'''; diff --git a/lib/src/timeline/styles.css.dart b/lib/src/timeline/styles.css.dart index 5971cae8..6ea3c61c 100644 --- a/lib/src/timeline/styles.css.dart +++ b/lib/src/timeline/styles.css.dart @@ -243,4 +243,4 @@ max-width: 800px; from {bottom: 30px; opacity: 1;} to {bottom: 0; opacity: 0;} } -'''; \ No newline at end of file +'''; From 3f75682958b90804c4d0fa573545c1efe711367b Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 14 Jun 2024 03:01:39 +0200 Subject: [PATCH 053/119] Fix format again --- lib/src/timeline/script.js.dart | 1 - lib/src/timeline/timeline.dart | 77 +++++++++++++++++++++------------ 2 files changed, 49 insertions(+), 29 deletions(-) diff --git a/lib/src/timeline/script.js.dart b/lib/src/timeline/script.js.dart index 6806b0a0..08e540da 100644 --- a/lib/src/timeline/script.js.dart +++ b/lib/src/timeline/script.js.dart @@ -1,4 +1,3 @@ - // language=javascript /// The script used in the HTML file that is generated for the timeline. const String timelineJS = ''' diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index cd1e0f17..7182ec20 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -12,7 +12,6 @@ import 'package:test_api/src/backend/invoker.dart'; //ignore: implementation_imports import 'package:test_api/src/backend/live_test.dart'; - final Map _timelines = {}; /// Returns the test name including the group hierarchy. @@ -24,14 +23,15 @@ String testNameWithHierarchy() { // Group names are concatenated with the name of the previous group final rawGroupNames = Invoker.current?.liveTest.groups - .map((group) { - if (group.name.isEmpty) { - return null; - } - return group.name; - }) - .whereNotNull() - .toList() ?? []; + .map((group) { + if (group.name.isEmpty) { + return null; + } + return group.name; + }) + .whereNotNull() + .toList() ?? + []; List removeRedundantParts(List inputList) { if (inputList.length < 2) { @@ -149,7 +149,11 @@ class Timeline { TimelineMode mode = TimelineMode.off; /// Adds a screenshot to the timeline. - void addScreenshot(Screenshot screenshot, {String? name, TimelineEventType? eventType,}) { + void addScreenshot( + Screenshot screenshot, { + String? name, + TimelineEventType? eventType, + }) { addEvent( TimelineEvent.now( name: name, @@ -188,7 +192,8 @@ class Timeline { } // always append a unique id to avoid name collisions final uniqueId = nanoid(length: 5); - final htmlFile = File(path.join(spotTempDir.path, 'timeline_events_$uniqueId.html')); + final htmlFile = + File(path.join(spotTempDir.path, 'timeline_events_$uniqueId.html')); final content = _timelineAsHTML(); htmlFile.writeAsStringSync(content); //ignore: avoid_print @@ -218,21 +223,25 @@ class Timeline { } /// Returns the events in the timeline as an HTML string. - String _timelineAsHTML(){ + String _timelineAsHTML() { final htmlBuffer = StringBuffer(); final nameWithHierarchy = testNameWithHierarchy(); - + htmlBuffer.writeln(''); htmlBuffer.writeln(''); htmlBuffer.writeln('Timeline Events'); - htmlBuffer.writeln(''); + htmlBuffer.writeln( + ''); - final String eventsForScript = _events.where((event) => event.screenshot != null).map((event) { + final String eventsForScript = + _events.where((event) => event.screenshot != null).map((event) { return '{src: "file://${event.screenshot!.file.path}", title: "${event.eventType?.label ?? "Event ${_events.indexOf(event) + 1}"}"}'; }).join(',\n '); htmlBuffer.writeln(''); @@ -243,21 +252,24 @@ class Timeline { htmlBuffer.writeln(''); htmlBuffer.writeln(''); htmlBuffer.writeln('
'); - htmlBuffer.writeln(''); + htmlBuffer.writeln( + ''); htmlBuffer.writeln('

Timeline

'); htmlBuffer.writeln('
'); htmlBuffer.writeln('

Info

'); htmlBuffer.writeln('

Test: $nameWithHierarchy

'); - htmlBuffer.writeln(''); + htmlBuffer.writeln( + ''); htmlBuffer.writeln('
'); - if(_events.isNotEmpty){ - htmlBuffer.writeln('

Events

'); + if (_events.isNotEmpty) { + htmlBuffer + .writeln('

Events

'); } - final events = (){ + final events = () { final eventBuffer = StringBuffer(); for (final event in _events) { final part = () { @@ -265,8 +277,12 @@ class Timeline { final caller = event.initiator != null ? 'at ${event.initiator!.member} ${event.initiator!.uri}:${event.initiator!.line}:${event.initiator!.column}' : 'N/A'; - final type = event.eventType != null ? event.eventType!.label : "Unknown event type"; - final screenshot = event.screenshot != null ? 'Screenshot' : ''; + final type = event.eventType != null + ? event.eventType!.label + : "Unknown event type"; + final screenshot = event.screenshot != null + ? 'Screenshot' + : ''; return '''

#${index + 1}

@@ -289,12 +305,16 @@ class Timeline { htmlBuffer.write(events); htmlBuffer.writeln(''); htmlBuffer.writeln(''); @@ -310,9 +330,10 @@ enum TimelineEventType { tap( 'Tap Event (crosshair indicator)', ); + const TimelineEventType(this.label); -/// The name of the event. + /// The name of the event. final String label; } From 57c02c3a13eff1ddfec180bb19738468d5670ba8 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 14 Jun 2024 03:02:38 +0200 Subject: [PATCH 054/119] Add missing trailing comma --- lib/src/timeline/timeline.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index 7182ec20..44d36925 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -231,7 +231,7 @@ class Timeline { htmlBuffer.writeln(''); htmlBuffer.writeln('Timeline Events'); htmlBuffer.writeln( - ''); + '',); final String eventsForScript = _events.where((event) => event.screenshot != null).map((event) { @@ -253,7 +253,7 @@ class Timeline { htmlBuffer.writeln(''); htmlBuffer.writeln('
'); htmlBuffer.writeln( - ''); + '',); htmlBuffer.writeln('

Timeline

'); htmlBuffer.writeln('
'); @@ -261,7 +261,7 @@ class Timeline { htmlBuffer.writeln('

Test: $nameWithHierarchy

'); htmlBuffer.writeln( - ''); + '',); htmlBuffer.writeln('
'); if (_events.isNotEmpty) { @@ -308,7 +308,7 @@ class Timeline { htmlBuffer .writeln('×'); htmlBuffer.writeln( - 'Screenshot of the Event'); + 'Screenshot of the Event',); htmlBuffer.writeln('
'); htmlBuffer .writeln(''); From c89b83a4806a7d68fcf3e6b98c43452775c4cb5a Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 14 Jun 2024 03:06:16 +0200 Subject: [PATCH 055/119] Format timeline --- lib/src/timeline/timeline.dart | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index 44d36925..15d83647 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -231,7 +231,8 @@ class Timeline { htmlBuffer.writeln(''); htmlBuffer.writeln('Timeline Events'); htmlBuffer.writeln( - '',); + '', + ); final String eventsForScript = _events.where((event) => event.screenshot != null).map((event) { @@ -253,7 +254,8 @@ class Timeline { htmlBuffer.writeln(''); htmlBuffer.writeln('
'); htmlBuffer.writeln( - '',); + '', + ); htmlBuffer.writeln('

Timeline

'); htmlBuffer.writeln('
'); @@ -261,7 +263,8 @@ class Timeline { htmlBuffer.writeln('

Test: $nameWithHierarchy

'); htmlBuffer.writeln( - '',); + '', + ); htmlBuffer.writeln('
'); if (_events.isNotEmpty) { @@ -308,7 +311,8 @@ class Timeline { htmlBuffer .writeln('×'); htmlBuffer.writeln( - 'Screenshot of the Event',); + 'Screenshot of the Event', + ); htmlBuffer.writeln('
'); htmlBuffer .writeln(''); From 11cc3a29a95ad7c1bd8e901a07966af645c43880 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 14 Jun 2024 03:20:48 +0200 Subject: [PATCH 056/119] Expect exitcode --- test/timeline/timeline_test.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index ddcc3627..9275a396 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -151,7 +151,6 @@ void main() async { }); } '''; - final testAsString = [importPart, widgetPart, testPart].join('\n'); final tempDir = Directory.systemTemp.createTempSync(); @@ -159,7 +158,7 @@ void main() async { await tempTestFile.writeAsString(testAsString); final testProcess = - await TestProcess.start('flutter', ['test', tempTestFile.path]); + await TestProcess.start('flutter', ['test', tempTestFile.path], runInShell: true,); final stdoutBuffer = StringBuffer(); @@ -176,7 +175,7 @@ void main() async { } } - await testProcess.shouldExit(); + await testProcess.shouldExit(1); tempDir.deleteSync(recursive: true); From e6aa9544d838d71bafdbb02dbbaf991a046d3f2e Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 14 Jun 2024 03:23:12 +0200 Subject: [PATCH 057/119] Format test --- test/timeline/timeline_test.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index 9275a396..e280de76 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -157,8 +157,11 @@ void main() async { final tempTestFile = File('${tempDir.path}/temp_test.dart'); await tempTestFile.writeAsString(testAsString); - final testProcess = - await TestProcess.start('flutter', ['test', tempTestFile.path], runInShell: true,); + final testProcess = await TestProcess.start( + 'flutter', + ['test', tempTestFile.path], + runInShell: true, + ); final stdoutBuffer = StringBuffer(); From 7ed19f4e5cf8ba50ef8b49dbca8a7ed266ccd559 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 14 Jun 2024 11:14:47 +0200 Subject: [PATCH 058/119] Do test experiment --- test/timeline/timeline_test.dart | 60 +++++++++++++++++++------------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index e280de76..2073cf24 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -152,38 +152,50 @@ void main() async { } '''; final testAsString = [importPart, widgetPart, testPart].join('\n'); +String result = ""; + final tempDir = Directory.systemTemp.createTempSync(); + try{ + final tempTestFile = File('${tempDir.path}/temp_test.dart'); + await tempTestFile.writeAsString(testAsString); - final tempDir = Directory.systemTemp.createTempSync(); - final tempTestFile = File('${tempDir.path}/temp_test.dart'); - await tempTestFile.writeAsString(testAsString); + final testProcess = await TestProcess.start( + 'flutter', + ['test', tempTestFile.path], + runInShell: true, + ); - final testProcess = await TestProcess.start( - 'flutter', - ['test', tempTestFile.path], - runInShell: true, - ); - - final stdoutBuffer = StringBuffer(); + final stdoutBuffer = StringBuffer(); - bool write = false; - await for (final line in testProcess.stdoutStream()) { - if (line == 'Timeline') { - write = true; - } - if (line.startsWith('To run this test again:')) { - write = false; - } - if (write) { - stdoutBuffer.writeln(line); + bool write = false; + await for (final line in testProcess.stdoutStream()) { + if (line == 'Timeline') { + write = true; + } + if (line.startsWith('To run this test again:')) { + write = false; + } + if (write) { + stdoutBuffer.writeln(line); + } } + + await testProcess.shouldExit(1); + result = stdoutBuffer.toString(); + }catch(_, __){ + + } + + if(tempDir.existsSync()){ + tempDir.deleteSync(recursive: true); } - await testProcess.shouldExit(1); - tempDir.deleteSync(recursive: true); + if(result.isEmpty) { + throw TestFailure('No output from test process'); + } - final stdout = stdoutBuffer.toString(); - final timeline = stdout.split('\n')..removeWhere((line) => line.isEmpty); + final timeline = result.split('\n')..removeWhere((line) => line.isEmpty); + expect(timeline.first, 'Timeline'); expect( timeline[1], From f1b9f14e7fff0349634f8a873004c14abf2caa4f Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 14 Jun 2024 11:15:47 +0200 Subject: [PATCH 059/119] Adjust to formatting --- test/timeline/timeline_test.dart | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index 2073cf24..2cd7779a 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -152,9 +152,9 @@ void main() async { } '''; final testAsString = [importPart, widgetPart, testPart].join('\n'); -String result = ""; - final tempDir = Directory.systemTemp.createTempSync(); - try{ + String result = ""; + final tempDir = Directory.systemTemp.createTempSync(); + try { final tempTestFile = File('${tempDir.path}/temp_test.dart'); await tempTestFile.writeAsString(testAsString); @@ -181,21 +181,18 @@ String result = ""; await testProcess.shouldExit(1); result = stdoutBuffer.toString(); - }catch(_, __){ + } catch (_, __) {} - } - - if(tempDir.existsSync()){ + if (tempDir.existsSync()) { tempDir.deleteSync(recursive: true); } - - if(result.isEmpty) { + if (result.isEmpty) { throw TestFailure('No output from test process'); } final timeline = result.split('\n')..removeWhere((line) => line.isEmpty); - + expect(timeline.first, 'Timeline'); expect( timeline[1], From 74993f0bbbc78ea5bba65bdf45380c63bfba6ace Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 14 Jun 2024 11:45:54 +0200 Subject: [PATCH 060/119] Rm html test temporarily --- test/timeline/timeline_test.dart | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index 2cd7779a..c92967b7 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -161,7 +161,6 @@ void main() async { final testProcess = await TestProcess.start( 'flutter', ['test', tempTestFile.path], - runInShell: true, ); final stdoutBuffer = StringBuffer(); @@ -222,10 +221,6 @@ void main() async { timeline[6], '========================================================', ); - expect( - timeline.last, - startsWith('View time line here:'), - ); }); }); } From 66651e23d1137dcbe1c615035cf6e0499859b29f Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 14 Jun 2024 11:52:18 +0200 Subject: [PATCH 061/119] Print timeline on test --- test/timeline/timeline_test.dart | 62 ++++++++++++++++---------------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index c92967b7..8d2c5c75 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import 'package:test_process/test_process.dart'; - import 'timeline_test_widget.dart'; final _addButtonSelector = spotIcon(Icons.add); @@ -151,47 +150,37 @@ void main() async { }); } '''; + final testAsString = [importPart, widgetPart, testPart].join('\n'); - String result = ""; + final tempDir = Directory.systemTemp.createTempSync(); - try { - final tempTestFile = File('${tempDir.path}/temp_test.dart'); - await tempTestFile.writeAsString(testAsString); + final tempTestFile = File('${tempDir.path}/temp_test.dart'); + await tempTestFile.writeAsString(testAsString); - final testProcess = await TestProcess.start( - 'flutter', - ['test', tempTestFile.path], - ); + final testProcess = + await TestProcess.start('flutter', ['test', tempTestFile.path]); - final stdoutBuffer = StringBuffer(); + final stdoutBuffer = StringBuffer(); - bool write = false; - await for (final line in testProcess.stdoutStream()) { - if (line == 'Timeline') { - write = true; - } - if (line.startsWith('To run this test again:')) { - write = false; - } - if (write) { - stdoutBuffer.writeln(line); - } + bool write = false; + await for (final line in testProcess.stdoutStream()) { + if (line == 'Timeline') { + write = true; + } + if (line.startsWith('To run this test again:')) { + write = false; + } + if (write) { + stdoutBuffer.writeln(line); } - - await testProcess.shouldExit(1); - result = stdoutBuffer.toString(); - } catch (_, __) {} - - if (tempDir.existsSync()) { - tempDir.deleteSync(recursive: true); } - if (result.isEmpty) { - throw TestFailure('No output from test process'); - } + await testProcess.shouldExit(); - final timeline = result.split('\n')..removeWhere((line) => line.isEmpty); + tempDir.deleteSync(recursive: true); + final stdout = stdoutBuffer.toString(); + final timeline = stdout.split('\n')..removeWhere((line) => line.isEmpty); expect(timeline.first, 'Timeline'); expect( timeline[1], @@ -221,6 +210,8 @@ void main() async { timeline[6], '========================================================', ); + // ignore: avoid_print + print('timeline: $timeline'); }); }); } @@ -269,3 +260,10 @@ Future _captureConsoleOutput( return buffer.toString(); } + +String _fileUrlToPath(String fileUrl) { + if (fileUrl.startsWith('file:///')) { + return Uri.decodeFull(fileUrl.substring(7)); + } + return fileUrl; +} From 46e42d15f10c5123ca013d9d81134d70df6a3a1a Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 14 Jun 2024 12:25:33 +0200 Subject: [PATCH 062/119] Add more tests --- lib/src/timeline/timeline.dart | 12 ++++++----- test/timeline/timeline_test.dart | 37 ++++++++++++++++---------------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index 15d83647..cd52fb70 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -1,7 +1,6 @@ // ignore_for_file: depend_on_referenced_packages import 'dart:io'; import 'package:collection/collection.dart'; -import 'package:nanoid2/nanoid2.dart'; import 'package:path/path.dart' as path; import 'package:spot/src/screenshot/screenshot.dart'; import 'package:spot/src/spot/tree_snapshot.dart'; @@ -187,13 +186,16 @@ class Timeline { void _printHTML() { final spotTempDir = Directory.systemTemp.createTempSync(); + final String name = (Invoker.current?.liveTest.test.name ?? '') + .trim() + .toLowerCase() + .replaceAll(' ', '_'); + if (name.isEmpty) return; if (!spotTempDir.existsSync()) { spotTempDir.createSync(); } - // always append a unique id to avoid name collisions - final uniqueId = nanoid(length: 5); - final htmlFile = - File(path.join(spotTempDir.path, 'timeline_events_$uniqueId.html')); + + final htmlFile = File(path.join(spotTempDir.path, 'timeline_$name.html')); final content = _timelineAsHTML(); htmlFile.writeAsStringSync(content); //ignore: avoid_print diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index 8d2c5c75..be1b7209 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -11,6 +11,9 @@ final _addButtonSelector = spotIcon(Icons.add); final _subtractButtonSelector = spotIcon(Icons.remove); final _clearButtonSelector = spotIcon(Icons.clear); +const _header = '==================== Timeline Event ===================='; +const _separator = '========================================================'; + void main() { testWidgets('Live timeline', (tester) async { final output = await _captureConsoleOutput(() async { @@ -164,27 +167,25 @@ void main() async { bool write = false; await for (final line in testProcess.stdoutStream()) { + if (line.isEmpty) continue; if (line == 'Timeline') { write = true; } - if (line.startsWith('To run this test again:')) { - write = false; - } if (write) { stdoutBuffer.writeln(line); } } await testProcess.shouldExit(); - tempDir.deleteSync(recursive: true); final stdout = stdoutBuffer.toString(); - final timeline = stdout.split('\n')..removeWhere((line) => line.isEmpty); + final timeline = stdout.split('\n'); + expect(timeline.first, 'Timeline'); expect( timeline[1], - '==================== Timeline Event ====================', + _header, ); expect( timeline[2], @@ -208,10 +209,17 @@ void main() async { ); expect( timeline[6], - '========================================================', + _separator, + ); + final htmlLine = timeline[timeline.lastIndexOf(_separator) + 1]; + expect( + htmlLine.startsWith( + 'View time line here:', + ), + isTrue, ); - // ignore: avoid_print - print('timeline: $timeline'); + final htmlName = htmlLine.split('/').last; + expect(htmlName, 'timeline_onerror_timeline_with_error.html'); }); }); } @@ -221,9 +229,7 @@ void _testTimeLineContent({ required int eventCount, }) { expect( - RegExp('==================== Timeline Event ====================') - .allMatches(output) - .length, + RegExp(_header).allMatches(output).length, eventCount, ); expect( @@ -260,10 +266,3 @@ Future _captureConsoleOutput( return buffer.toString(); } - -String _fileUrlToPath(String fileUrl) { - if (fileUrl.startsWith('file:///')) { - return Uri.decodeFull(fileUrl.substring(7)); - } - return fileUrl; -} From addb30868126172fcf67172201672a844388e75d Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 14 Jun 2024 13:52:36 +0200 Subject: [PATCH 063/119] Print html for live timeline when no error happens --- lib/src/timeline/timeline.dart | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index cd52fb70..82ec4f10 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -117,9 +117,14 @@ Timeline currentTimeline() { final newTimeline = Timeline(); Invoker.current!.addTearDown(() { - if (newTimeline.mode == TimelineMode.record && - !test.state.result.isPassing) { - newTimeline.printToConsole(); + if (!test.state.result.isPassing) { + if (newTimeline.mode == TimelineMode.record || + newTimeline.mode == TimelineMode.live) { + newTimeline.printToConsole(); + newTimeline.printHTML(); + } + } else if (newTimeline.mode == TimelineMode.live) { + newTimeline.printHTML(); } }); _timelines[test] = newTimeline; @@ -174,17 +179,17 @@ class Timeline { } } - /// Prints the timeline to the console. + /// Prints the complete timeline to the console. void printToConsole() { // ignore: avoid_print print('Timeline'); for (final event in _events) { _printEvent(event); } - _printHTML(); } - void _printHTML() { + /// Prints the timeline as an HTML file. + void printHTML() { final spotTempDir = Directory.systemTemp.createTempSync(); final String name = (Invoker.current?.liveTest.test.name ?? '') .trim() From 9d6034b92d9d8e181538ef69d4006ac0809caa32 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 14 Jun 2024 13:53:08 +0200 Subject: [PATCH 064/119] Add test "Live timeline - without error, prints HTML" --- test/timeline/timeline_test.dart | 100 ++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 2 deletions(-) diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index be1b7209..0296bdb4 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:dartx/dartx.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; @@ -98,7 +99,7 @@ void main() { expect(output, contains('鈴革笌 - Timeline recording is off')); }); - group('onError timeline', () { + group('Print on teardown', () { testWidgets('OnError timeline - without error', (tester) async { final output = await _captureConsoleOutput(() async { recordOnErrorTimeline(); @@ -176,7 +177,7 @@ void main() async { } } - await testProcess.shouldExit(); + await testProcess.shouldExit(1); tempDir.deleteSync(recursive: true); final stdout = stdoutBuffer.toString(); @@ -221,6 +222,101 @@ void main() async { final htmlName = htmlLine.split('/').last; expect(htmlName, 'timeline_onerror_timeline_with_error.html'); }); + test('Live timeline - without error, prints HTML', () async { + const importPart = ''' +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; +import 'package:spot/src/timeline/timeline.dart'; +'''; + + final widgetPart = + File('test/timeline/timeline_test_widget.dart').readAsStringSync(); + + const testPart = ''' +void main() async { + final addButtonSelector = spotIcon(Icons.add); + final subtractButtonSelector = spotIcon(Icons.remove); + testWidgets('Live timeline without error prints html', (WidgetTester tester) async { + recordLiveTimeline(); + await tester.pumpWidget(const TimelineTestWidget()); + addButtonSelector.existsOnce(); + spotText('Counter: 3').existsOnce(); + await act.tap(addButtonSelector); + spotText('Counter: 4').existsOnce(); + await act.tap(subtractButtonSelector); + spotText('Counter: 3').existsOnce(); + }); +} +'''; + + final testAsString = [importPart, widgetPart, testPart].join('\n'); + + final tempDir = Directory.systemTemp.createTempSync(); + final tempTestFile = File('${tempDir.path}/temp_test.dart'); + await tempTestFile.writeAsString(testAsString); + + final testProcess = + await TestProcess.start('flutter', ['test', tempTestFile.path]); + + final stdoutBuffer = StringBuffer(); + + bool write = false; + await for (final line in testProcess.stdoutStream()) { + if (line.isEmpty) continue; + + if (!write) { + if (line == _header) { + write = true; + } + } + + if (write) { + stdoutBuffer.writeln(line); + } + } + + // Error does not happen + await testProcess.shouldExit(0); + tempDir.deleteSync(recursive: true); + + final stdout = stdoutBuffer.toString(); + final timeline = stdout.split('\n'); + + // Does not start with 'Timeline', this only happens on error + expect(timeline.first, _header); + expect( + timeline.second, + 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', + ); + expect( + timeline[2].startsWith('Caller: at main. file:///'), + isTrue, + ); + expect( + timeline[3].startsWith( + 'Screenshot: file:///', + ), + isTrue, + ); + expect( + timeline[4].startsWith( + 'Timestamp:', + ), + isTrue, + ); + expect( + timeline[5], + _separator, + ); + final htmlLine = timeline + .firstWhere((line) => line.startsWith('View time line here:')); + expect( + htmlLine.endsWith( + 'timeline_live_timeline_without_error_prints_html.html', + ), + isTrue, + ); + }); }); } From 2ff736e04bf544f669603e27ba09b1ec0362cf29 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 14 Jun 2024 13:53:25 +0200 Subject: [PATCH 065/119] Revert "Add test "Live timeline - without error, prints HTML"" This reverts commit 9d6034b92d9d8e181538ef69d4006ac0809caa32. --- test/timeline/timeline_test.dart | 100 +------------------------------ 1 file changed, 2 insertions(+), 98 deletions(-) diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index 0296bdb4..be1b7209 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:io'; -import 'package:dartx/dartx.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; @@ -99,7 +98,7 @@ void main() { expect(output, contains('鈴革笌 - Timeline recording is off')); }); - group('Print on teardown', () { + group('onError timeline', () { testWidgets('OnError timeline - without error', (tester) async { final output = await _captureConsoleOutput(() async { recordOnErrorTimeline(); @@ -177,7 +176,7 @@ void main() async { } } - await testProcess.shouldExit(1); + await testProcess.shouldExit(); tempDir.deleteSync(recursive: true); final stdout = stdoutBuffer.toString(); @@ -222,101 +221,6 @@ void main() async { final htmlName = htmlLine.split('/').last; expect(htmlName, 'timeline_onerror_timeline_with_error.html'); }); - test('Live timeline - without error, prints HTML', () async { - const importPart = ''' -import 'package:flutter_test/flutter_test.dart'; -import 'package:spot/spot.dart'; -import 'package:spot/src/timeline/timeline.dart'; -'''; - - final widgetPart = - File('test/timeline/timeline_test_widget.dart').readAsStringSync(); - - const testPart = ''' -void main() async { - final addButtonSelector = spotIcon(Icons.add); - final subtractButtonSelector = spotIcon(Icons.remove); - testWidgets('Live timeline without error prints html', (WidgetTester tester) async { - recordLiveTimeline(); - await tester.pumpWidget(const TimelineTestWidget()); - addButtonSelector.existsOnce(); - spotText('Counter: 3').existsOnce(); - await act.tap(addButtonSelector); - spotText('Counter: 4').existsOnce(); - await act.tap(subtractButtonSelector); - spotText('Counter: 3').existsOnce(); - }); -} -'''; - - final testAsString = [importPart, widgetPart, testPart].join('\n'); - - final tempDir = Directory.systemTemp.createTempSync(); - final tempTestFile = File('${tempDir.path}/temp_test.dart'); - await tempTestFile.writeAsString(testAsString); - - final testProcess = - await TestProcess.start('flutter', ['test', tempTestFile.path]); - - final stdoutBuffer = StringBuffer(); - - bool write = false; - await for (final line in testProcess.stdoutStream()) { - if (line.isEmpty) continue; - - if (!write) { - if (line == _header) { - write = true; - } - } - - if (write) { - stdoutBuffer.writeln(line); - } - } - - // Error does not happen - await testProcess.shouldExit(0); - tempDir.deleteSync(recursive: true); - - final stdout = stdoutBuffer.toString(); - final timeline = stdout.split('\n'); - - // Does not start with 'Timeline', this only happens on error - expect(timeline.first, _header); - expect( - timeline.second, - 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', - ); - expect( - timeline[2].startsWith('Caller: at main. file:///'), - isTrue, - ); - expect( - timeline[3].startsWith( - 'Screenshot: file:///', - ), - isTrue, - ); - expect( - timeline[4].startsWith( - 'Timestamp:', - ), - isTrue, - ); - expect( - timeline[5], - _separator, - ); - final htmlLine = timeline - .firstWhere((line) => line.startsWith('View time line here:')); - expect( - htmlLine.endsWith( - 'timeline_live_timeline_without_error_prints_html.html', - ), - isTrue, - ); - }); }); } From ae7782addc1abfaf01b28863e83d64cbcfda87b0 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 14 Jun 2024 13:55:05 +0200 Subject: [PATCH 066/119] Add test Live timeline - without error, prints HTML --- test/timeline/timeline_test.dart | 100 ++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 2 deletions(-) diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index be1b7209..0296bdb4 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:dartx/dartx.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; @@ -98,7 +99,7 @@ void main() { expect(output, contains('鈴革笌 - Timeline recording is off')); }); - group('onError timeline', () { + group('Print on teardown', () { testWidgets('OnError timeline - without error', (tester) async { final output = await _captureConsoleOutput(() async { recordOnErrorTimeline(); @@ -176,7 +177,7 @@ void main() async { } } - await testProcess.shouldExit(); + await testProcess.shouldExit(1); tempDir.deleteSync(recursive: true); final stdout = stdoutBuffer.toString(); @@ -221,6 +222,101 @@ void main() async { final htmlName = htmlLine.split('/').last; expect(htmlName, 'timeline_onerror_timeline_with_error.html'); }); + test('Live timeline - without error, prints HTML', () async { + const importPart = ''' +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; +import 'package:spot/src/timeline/timeline.dart'; +'''; + + final widgetPart = + File('test/timeline/timeline_test_widget.dart').readAsStringSync(); + + const testPart = ''' +void main() async { + final addButtonSelector = spotIcon(Icons.add); + final subtractButtonSelector = spotIcon(Icons.remove); + testWidgets('Live timeline without error prints html', (WidgetTester tester) async { + recordLiveTimeline(); + await tester.pumpWidget(const TimelineTestWidget()); + addButtonSelector.existsOnce(); + spotText('Counter: 3').existsOnce(); + await act.tap(addButtonSelector); + spotText('Counter: 4').existsOnce(); + await act.tap(subtractButtonSelector); + spotText('Counter: 3').existsOnce(); + }); +} +'''; + + final testAsString = [importPart, widgetPart, testPart].join('\n'); + + final tempDir = Directory.systemTemp.createTempSync(); + final tempTestFile = File('${tempDir.path}/temp_test.dart'); + await tempTestFile.writeAsString(testAsString); + + final testProcess = + await TestProcess.start('flutter', ['test', tempTestFile.path]); + + final stdoutBuffer = StringBuffer(); + + bool write = false; + await for (final line in testProcess.stdoutStream()) { + if (line.isEmpty) continue; + + if (!write) { + if (line == _header) { + write = true; + } + } + + if (write) { + stdoutBuffer.writeln(line); + } + } + + // Error does not happen + await testProcess.shouldExit(0); + tempDir.deleteSync(recursive: true); + + final stdout = stdoutBuffer.toString(); + final timeline = stdout.split('\n'); + + // Does not start with 'Timeline', this only happens on error + expect(timeline.first, _header); + expect( + timeline.second, + 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', + ); + expect( + timeline[2].startsWith('Caller: at main. file:///'), + isTrue, + ); + expect( + timeline[3].startsWith( + 'Screenshot: file:///', + ), + isTrue, + ); + expect( + timeline[4].startsWith( + 'Timestamp:', + ), + isTrue, + ); + expect( + timeline[5], + _separator, + ); + final htmlLine = timeline + .firstWhere((line) => line.startsWith('View time line here:')); + expect( + htmlLine.endsWith( + 'timeline_live_timeline_without_error_prints_html.html', + ), + isTrue, + ); + }); }); } From 6a69f73701a3a199da9d5b422be9eab339017698 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 14 Jun 2024 15:47:23 +0200 Subject: [PATCH 067/119] Refactor test method --- test/timeline/timeline_test.dart | 126 +++++++++++++++---------------- 1 file changed, 60 insertions(+), 66 deletions(-) diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index 0296bdb4..af52c8e6 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -5,6 +5,7 @@ import 'package:dartx/dartx.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; +import 'package:spot/src/timeline/timeline.dart'; import 'package:test_process/test_process.dart'; import 'timeline_test_widget.dart'; @@ -15,6 +16,47 @@ final _clearButtonSelector = spotIcon(Icons.clear); const _header = '==================== Timeline Event ===================='; const _separator = '========================================================'; +String _testAsString({ + required String title, + required TimelineMode timelineMode, + bool shouldFail = false, +}) { + final String methodForMode = () { + switch (timelineMode) { + case TimelineMode.live: + return 'recordLiveTimeline()'; + case TimelineMode.record: + return 'recordOnErrorTimeline()'; + case TimelineMode.off: + return 'stopRecordingTimeline()'; + } + }(); + + final widgetPart = + File('test/timeline/timeline_test_widget.dart').readAsStringSync(); + return ''' +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; +import 'package:spot/src/timeline/timeline.dart';\n +$widgetPart\n +void main() async { + final addButtonSelector = spotIcon(Icons.add); + final subtractButtonSelector = spotIcon(Icons.remove); + testWidgets("$title", (WidgetTester tester) async { + $methodForMode; + await tester.pumpWidget(const TimelineTestWidget()); + addButtonSelector.existsOnce(); + spotText('Counter: 3').existsOnce(); + await act.tap(addButtonSelector); + spotText('Counter: 4').existsOnce(); + await act.tap(subtractButtonSelector); + spotText('Counter: 3').existsOnce(); + ${shouldFail ? 'spotText("Counter: 99").existsOnce();' : ''} + }); +} +'''; +} + void main() { testWidgets('Live timeline', (tester) async { final output = await _captureConsoleOutput(() async { @@ -127,39 +169,15 @@ void main() { }); test('OnError timeline - with error, prints timeline', () async { - const importPart = ''' -import 'package:flutter_test/flutter_test.dart'; -import 'package:spot/spot.dart'; -import 'package:spot/src/timeline/timeline.dart'; -'''; - - final widgetPart = - File('test/timeline/timeline_test_widget.dart').readAsStringSync(); - - const testPart = ''' -void main() async { - final addButtonSelector = spotIcon(Icons.add); - final subtractButtonSelector = spotIcon(Icons.remove); - testWidgets('OnError timeline with error', (WidgetTester tester) async { - recordOnErrorTimeline(); - await tester.pumpWidget(const TimelineTestWidget()); - addButtonSelector.existsOnce(); - spotText('Counter: 3').existsOnce(); - await act.tap(addButtonSelector); - spotText('Counter: 4').existsOnce(); - await act.tap(subtractButtonSelector); - spotText('Counter: 3').existsOnce(); - // Make test fail intentionally - spotText('Counter: 99').existsOnce(); - }); -} -'''; - - final testAsString = [importPart, widgetPart, testPart].join('\n'); - final tempDir = Directory.systemTemp.createTempSync(); final tempTestFile = File('${tempDir.path}/temp_test.dart'); - await tempTestFile.writeAsString(testAsString); + await tempTestFile.writeAsString( + _testAsString( + title: 'OnError timeline with error', + timelineMode: TimelineMode.record, + shouldFail: true, + ), + ); final testProcess = await TestProcess.start('flutter', ['test', tempTestFile.path]); @@ -177,6 +195,7 @@ void main() async { } } + // Error happens await testProcess.shouldExit(1); tempDir.deleteSync(recursive: true); @@ -212,48 +231,24 @@ void main() async { timeline[6], _separator, ); - final htmlLine = timeline[timeline.lastIndexOf(_separator) + 1]; + final htmlLine = timeline + .firstWhere((line) => line.startsWith('View time line here:')); expect( - htmlLine.startsWith( - 'View time line here:', + htmlLine.endsWith( + 'timeline_onerror_timeline_with_error.html', ), isTrue, ); - final htmlName = htmlLine.split('/').last; - expect(htmlName, 'timeline_onerror_timeline_with_error.html'); }); test('Live timeline - without error, prints HTML', () async { - const importPart = ''' -import 'package:flutter_test/flutter_test.dart'; -import 'package:spot/spot.dart'; -import 'package:spot/src/timeline/timeline.dart'; -'''; - - final widgetPart = - File('test/timeline/timeline_test_widget.dart').readAsStringSync(); - - const testPart = ''' -void main() async { - final addButtonSelector = spotIcon(Icons.add); - final subtractButtonSelector = spotIcon(Icons.remove); - testWidgets('Live timeline without error prints html', (WidgetTester tester) async { - recordLiveTimeline(); - await tester.pumpWidget(const TimelineTestWidget()); - addButtonSelector.existsOnce(); - spotText('Counter: 3').existsOnce(); - await act.tap(addButtonSelector); - spotText('Counter: 4').existsOnce(); - await act.tap(subtractButtonSelector); - spotText('Counter: 3').existsOnce(); - }); -} -'''; - - final testAsString = [importPart, widgetPart, testPart].join('\n'); - final tempDir = Directory.systemTemp.createTempSync(); final tempTestFile = File('${tempDir.path}/temp_test.dart'); - await tempTestFile.writeAsString(testAsString); + await tempTestFile.writeAsString( + _testAsString( + title: 'Live timeline without error prints html', + timelineMode: TimelineMode.live, + ), + ); final testProcess = await TestProcess.start('flutter', ['test', tempTestFile.path]); @@ -281,7 +276,6 @@ void main() async { final stdout = stdoutBuffer.toString(); final timeline = stdout.split('\n'); - // Does not start with 'Timeline', this only happens on error expect(timeline.first, _header); expect( From 9bfa63d9577aa4bc600bc22269ed64d03ece18be Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 14 Jun 2024 17:12:45 +0200 Subject: [PATCH 068/119] Do not print events twice on throw --- lib/src/timeline/timeline.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index 82ec4f10..ad39058d 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -117,13 +117,13 @@ Timeline currentTimeline() { final newTimeline = Timeline(); Invoker.current!.addTearDown(() { - if (!test.state.result.isPassing) { - if (newTimeline.mode == TimelineMode.record || - newTimeline.mode == TimelineMode.live) { - newTimeline.printToConsole(); - newTimeline.printHTML(); - } + if (!test.state.result.isPassing && + newTimeline.mode == TimelineMode.record) { + newTimeline.printToConsole(); + newTimeline.printHTML(); } else if (newTimeline.mode == TimelineMode.live) { + // printToConsole() here would lead to duplicate output since + // the timeline is already being printed live newTimeline.printHTML(); } }); From 8f711726e118dd53f38679135e21f244431a599e Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 14 Jun 2024 17:23:58 +0200 Subject: [PATCH 069/119] Add test "Live timeline - with error, no duplicates, prints HTML" --- test/timeline/timeline_test.dart | 72 ++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/test/timeline/timeline_test.dart b/test/timeline/timeline_test.dart index af52c8e6..28e5a7cc 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/timeline_test.dart @@ -311,6 +311,78 @@ void main() { isTrue, ); }); + test('Live timeline - with error, no duplicates, prints HTML', () async { + final tempDir = Directory.systemTemp.createTempSync(); + final tempTestFile = File('${tempDir.path}/temp_test.dart'); + await tempTestFile.writeAsString( + _testAsString( + title: 'Live timeline - with error, no duplicates, prints HTML', + timelineMode: TimelineMode.live, + shouldFail: true, + ), + ); + + final testProcess = + await TestProcess.start('flutter', ['test', tempTestFile.path]); + + final stdoutBuffer = StringBuffer(); + + bool write = false; + await for (final line in testProcess.stdoutStream()) { + if (line.isEmpty) continue; + + if (!write) { + if (line == _header) { + write = true; + } + } + + if (write) { + stdoutBuffer.writeln(line); + } + } + + // Error does not happen + await testProcess.shouldExit(1); + tempDir.deleteSync(recursive: true); + + final stdout = stdoutBuffer.toString(); + final timeline = stdout.split('\n'); + // Does not start with 'Timeline', this only happens on error + expect(timeline.first, _header); + expect( + timeline.second, + 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', + ); + expect( + timeline[2].startsWith('Caller: at main. file:///'), + isTrue, + ); + expect( + timeline[3].startsWith( + 'Screenshot: file:///', + ), + isTrue, + ); + expect( + timeline[4].startsWith( + 'Timestamp:', + ), + isTrue, + ); + expect( + timeline[5], + _separator, + ); + final htmlLine = timeline + .firstWhere((line) => line.startsWith('View time line here:')); + expect( + htmlLine.endsWith( + 'live_timeline_-_with_error,_no_duplicates,_prints_html.html', + ), + isTrue, + ); + }); }); } From 69ae48c834b7ce7553f5cb1c14728fa14a2d8888 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 14 Jun 2024 18:06:38 +0200 Subject: [PATCH 070/119] Move test_process to dev deps --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index e956f192..5a7684b8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,8 +23,8 @@ dependencies: test: ^1.24.0 test_api: '>=0.5.0 <0.8.0' stack_trace: ^1.11.0 - test_process: ^2.1.0 dev_dependencies: image: ^4.0.0 lint: ^2.1.0 + test_process: ^2.1.0 From 5ee4a51fedc0c726cd4dd2740b50e67c94caca44 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 14 Jun 2024 18:10:02 +0200 Subject: [PATCH 071/119] Make private wherever possible --- lib/src/timeline/timeline.dart | 126 ++++++++++++++++----------------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index ad39058d..b2cfe970 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -13,63 +13,6 @@ import 'package:test_api/src/backend/live_test.dart'; final Map _timelines = {}; -/// Returns the test name including the group hierarchy. -String testNameWithHierarchy() { - final test = Invoker.current?.liveTest; - if (test == null) { - return 'No test found'; - } - - // Group names are concatenated with the name of the previous group - final rawGroupNames = Invoker.current?.liveTest.groups - .map((group) { - if (group.name.isEmpty) { - return null; - } - return group.name; - }) - .whereNotNull() - .toList() ?? - []; - - List removeRedundantParts(List inputList) { - if (inputList.length < 2) { - return inputList; - } - - final List outputList = []; - for (int i = 0; i < inputList.length - 1; i++) { - outputList.add(inputList[i]); - } - - String lastElement = inputList.last; - final String previousElement = inputList[inputList.length - 2]; - - // Remove the part of the last element that is included in the previous one - if (lastElement.startsWith(previousElement)) { - lastElement = lastElement.substring(previousElement.length).trim(); - } - - if (lastElement.isNotEmpty) { - outputList.add(lastElement); - } - - return outputList; - } - - final cleanedGroups = removeRedundantParts(rawGroupNames); - if (cleanedGroups.isNotEmpty) { - final joinedGroups = cleanedGroups.join(' '); - - final List fullNameParts = [joinedGroups, test.test.name]; - final String finalTestName = removeRedundantParts(fullNameParts).last; - final String groupHierarchy = cleanedGroups.join(' => '); - return '$finalTestName in group(s): $groupHierarchy'; - } else { - return test.test.name; - } -} - /// Records the timeline and prints events as they happen. void recordLiveTimeline() { final timeline = currentTimeline(); @@ -120,11 +63,11 @@ Timeline currentTimeline() { if (!test.state.result.isPassing && newTimeline.mode == TimelineMode.record) { newTimeline.printToConsole(); - newTimeline.printHTML(); + newTimeline._printHTML(); } else if (newTimeline.mode == TimelineMode.live) { // printToConsole() here would lead to duplicate output since // the timeline is already being printed live - newTimeline.printHTML(); + newTimeline._printHTML(); } }); _timelines[test] = newTimeline; @@ -158,7 +101,7 @@ class Timeline { String? name, TimelineEventType? eventType, }) { - addEvent( + _addEvent( TimelineEvent.now( name: name, screenshot: screenshot, @@ -169,7 +112,7 @@ class Timeline { } /// Adds an event to the timeline. - void addEvent(TimelineEvent event) { + void _addEvent(TimelineEvent event) { if (mode == TimelineMode.off) { return; } @@ -189,7 +132,7 @@ class Timeline { } /// Prints the timeline as an HTML file. - void printHTML() { + void _printHTML() { final spotTempDir = Directory.systemTemp.createTempSync(); final String name = (Invoker.current?.liveTest.test.name ?? '') .trim() @@ -232,7 +175,7 @@ class Timeline { /// Returns the events in the timeline as an HTML string. String _timelineAsHTML() { final htmlBuffer = StringBuffer(); - final nameWithHierarchy = testNameWithHierarchy(); + final nameWithHierarchy = _testNameWithHierarchy(); htmlBuffer.writeln(''); htmlBuffer.writeln(''); @@ -411,3 +354,60 @@ enum TimelineMode { /// The timeline is not recording. off, } + +/// Returns the test name including the group hierarchy. +String _testNameWithHierarchy() { + final test = Invoker.current?.liveTest; + if (test == null) { + return 'No test found'; + } + + // Group names are concatenated with the name of the previous group + final rawGroupNames = Invoker.current?.liveTest.groups + .map((group) { + if (group.name.isEmpty) { + return null; + } + return group.name; + }) + .whereNotNull() + .toList() ?? + []; + + List removeRedundantParts(List inputList) { + if (inputList.length < 2) { + return inputList; + } + + final List outputList = []; + for (int i = 0; i < inputList.length - 1; i++) { + outputList.add(inputList[i]); + } + + String lastElement = inputList.last; + final String previousElement = inputList[inputList.length - 2]; + + // Remove the part of the last element that is included in the previous one + if (lastElement.startsWith(previousElement)) { + lastElement = lastElement.substring(previousElement.length).trim(); + } + + if (lastElement.isNotEmpty) { + outputList.add(lastElement); + } + + return outputList; + } + + final cleanedGroups = removeRedundantParts(rawGroupNames); + if (cleanedGroups.isNotEmpty) { + final joinedGroups = cleanedGroups.join(' '); + + final List fullNameParts = [joinedGroups, test.test.name]; + final String finalTestName = removeRedundantParts(fullNameParts).last; + final String groupHierarchy = cleanedGroups.join(' => '); + return '$finalTestName in group(s): $groupHierarchy'; + } else { + return test.test.name; + } +} From f93c36a270f77d8576269f585cba21d69d540bda Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 14 Jun 2024 18:30:18 +0200 Subject: [PATCH 072/119] Finalize modal layout --- lib/src/screenshot/screenshot.dart | 2 +- lib/src/timeline/styles.css.dart | 14 ++++++++++---- lib/src/timeline/timeline.dart | 10 +++++----- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/lib/src/screenshot/screenshot.dart b/lib/src/screenshot/screenshot.dart index ef587765..3e65ed63 100644 --- a/lib/src/screenshot/screenshot.dart +++ b/lib/src/screenshot/screenshot.dart @@ -171,7 +171,6 @@ Future _createScreenshot({ String callerFileName() { final file = frame?.uri.pathSegments.last.replaceFirst('.dart', ''); final line = frame?.line; - // escape / if (file != null && line != null) { return '$file:$line'; } @@ -184,6 +183,7 @@ Future _createScreenshot({ final String screenshotFileName = () { final String n; if (name != null) { + // escape / n = Uri.encodeQueryComponent(name); } else { n = callerFileName(); diff --git a/lib/src/timeline/styles.css.dart b/lib/src/timeline/styles.css.dart index 6ea3c61c..2eae9d60 100644 --- a/lib/src/timeline/styles.css.dart +++ b/lib/src/timeline/styles.css.dart @@ -106,10 +106,13 @@ h1 { height: 100%; overflow: auto; background-color: var(--modal-background-color); + justify-content: center; + align-items: center; } .modal img { -max-width: 800px; + max-width: 100%; + height: auto; } .modal span { @@ -118,11 +121,13 @@ max-width: 800px; .modal-content { margin: auto; - display: block; + display: flex; + flex-direction: column; max-width: 80%; height: auto; background-color: transparent; border: none; + position: relative; } .close { @@ -151,6 +156,7 @@ max-width: 800px; background-color: var(--nav-background-color); transform: translateY(-50%); padding: 10px; + margin: 0 5px; } .nav:hover { @@ -158,11 +164,11 @@ max-width: 800px; } .nav-left { - left: 0; + left: -50px; } .nav-right { - right: 0; + right: -50px; } #caption { diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index b2cfe970..c63551c2 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -260,17 +260,17 @@ class Timeline { htmlBuffer.writeln(''); // close caption + htmlBuffer.writeln('
'); // close modal-content + htmlBuffer.writeln('
'); // close modal htmlBuffer.writeln(''); htmlBuffer.writeln(''); From 1962f7a5609ce7a95abefab342da771744697cc2 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Tue, 18 Jun 2024 04:01:02 +0200 Subject: [PATCH 073/119] Translate hit position to image size --- lib/src/screenshot/screenshot_annotator.dart | 30 ++++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/lib/src/screenshot/screenshot_annotator.dart b/lib/src/screenshot/screenshot_annotator.dart index d2241352..7d648996 100644 --- a/lib/src/screenshot/screenshot_annotator.dart +++ b/lib/src/screenshot/screenshot_annotator.dart @@ -1,6 +1,6 @@ import 'dart:core'; import 'dart:ui' as ui; -import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; /// An annotator that can draw on a screenshot image. abstract class ScreenshotAnnotator { @@ -22,8 +22,26 @@ class CrosshairAnnotator implements ScreenshotAnnotator { /// Creates a [CrosshairAnnotator] with a [centerPosition]. const CrosshairAnnotator({required this.centerPosition}); + Offset _translateOffset(ui.Image image) { + // ignore: deprecated_member_use + final view = WidgetsBinding.instance.renderView.size; + final imageHeight = image.height; + final imageWidth = image.width; + // Calculate the relative position in the view + final double relativeX = centerPosition.dx / view.width; + final double relativeY = centerPosition.dy / view.height; + + // Calculate the new offset in the image dimensions + final double newDx = relativeX * imageWidth; + final double newDy = relativeY * imageHeight; + + return Offset(newDx, newDy); + } + @override Future annotate(ui.Image image) async { + final position = _translateOffset(image); + final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); @@ -34,14 +52,14 @@ class CrosshairAnnotator implements ScreenshotAnnotator { ..strokeWidth = 2.0; canvas.drawLine( - Offset(centerPosition.dx, centerPosition.dy - 20), - Offset(centerPosition.dx, centerPosition.dy + 20), + Offset(position.dx, position.dy - 20), + Offset(position.dx, position.dy + 20), paint, ); canvas.drawLine( - Offset(centerPosition.dx - 20, centerPosition.dy), - Offset(centerPosition.dx + 20, centerPosition.dy), + Offset(position.dx - 20, position.dy), + Offset(position.dx + 20, position.dy), paint, ); @@ -49,7 +67,7 @@ class CrosshairAnnotator implements ScreenshotAnnotator { ..color = const Color(0xFF00FFFF) ..style = PaintingStyle.stroke ..strokeWidth = 2.0; - canvas.drawCircle(centerPosition, 10.0, circlePaint); + canvas.drawCircle(position, 10.0, circlePaint); final picture = recorder.endRecording(); return picture.toImage(image.width, image.height); From 44e7dc9d0ddce4791405350c21d579d724f61ff0 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 19 Jun 2024 17:38:23 +0200 Subject: [PATCH 074/119] Add events to dragUntilVisible --- lib/src/act/act.dart | 35 +++++++++++++++++++++++++++++++--- lib/src/timeline/timeline.dart | 5 +++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/lib/src/act/act.dart b/lib/src/act/act.dart index db875dc3..2ef7719f 100644 --- a/lib/src/act/act.dart +++ b/lib/src/act/act.dart @@ -89,11 +89,11 @@ class Act { final centerPosition = renderBox.localToGlobal(renderBox.size.center(Offset.zero)); - final timeline = currentTimeline(); if (timeline.mode != TimelineMode.off) { - final screenshot = - await takeScreenshotWithCrosshair(centerPosition: centerPosition); + final screenshot = await takeScreenshotWithCrosshair( + centerPosition: centerPosition, + ); timeline.addScreenshot( screenshot, name: 'Tap ${selector.toStringBreadcrumb()}', @@ -174,14 +174,37 @@ class Act { final dragPosition = renderBox.localToGlobal(renderBox.size.center(Offset.zero)); + Future addDragEvent({ + required String name, + }) async { + final timeline = currentTimeline(); + if (timeline.mode != TimelineMode.off) { + final screenshot = await takeScreenshotWithCrosshair( + centerPosition: dragPosition, + ); + timeline.addScreenshot( + screenshot, + name: name, + eventType: TimelineEventType.drag, + ); + } + } + final targetName = dragTarget.toStringBreadcrumb(); bool isVisible = isTargetVisible(); if (isVisible) { + await addDragEvent( + name: 'Widget $targetName found without dragging.', + ); return; } + await addDragEvent( + name: 'Starting to drag $targetName at $dragPosition', + ); + int iterations = 0; while (iterations < maxIteration && !isVisible) { await gestures.drag(dragPosition, moveStep); @@ -191,6 +214,12 @@ class Act { } final totalDragged = moveStep * iterations.toDouble(); + final resultString = isVisible ? '' : 'not'; + final message = + "Target $targetName $resultString found after $iterations drags. " + "Total dragged offset: $totalDragged"; + + await addDragEvent(name: message); if (!isVisible) { throw TestFailure( diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index c63551c2..b1710631 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -283,6 +283,11 @@ enum TimelineEventType { /// A tap event. tap( 'Tap Event (crosshair indicator)', + ), + + /// A drag event. + drag( + 'Drag Event (crosshair indicator)', ); const TimelineEventType(this.label); From dc38abe8424897c82a8840b7441cbab56854fa70 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Thu, 20 Jun 2024 02:01:18 +0200 Subject: [PATCH 075/119] Add first drag timeline test --- lib/src/act/act.dart | 2 +- test/act/act_drag_test.dart | 53 +-- .../drag/drag_until_visible_test_widget.dart | 47 ++ test/timeline/drag/timeline_drag_test.dart | 439 ++++++++++++++++++ .../timeline_tap_test.dart} | 6 +- .../timeline_tap_test_widget.dart} | 0 6 files changed, 495 insertions(+), 52 deletions(-) create mode 100644 test/timeline/drag/drag_until_visible_test_widget.dart create mode 100644 test/timeline/drag/timeline_drag_test.dart rename test/timeline/{timeline_test.dart => tap/timeline_tap_test.dart} (98%) rename test/timeline/{timeline_test_widget.dart => tap/timeline_tap_test_widget.dart} (100%) diff --git a/lib/src/act/act.dart b/lib/src/act/act.dart index 2ef7719f..e049e5c0 100644 --- a/lib/src/act/act.dart +++ b/lib/src/act/act.dart @@ -202,7 +202,7 @@ class Act { } await addDragEvent( - name: 'Starting to drag $targetName at $dragPosition', + name: 'Starting to drag from $dragPosition towards $targetName.', ); int iterations = 0; diff --git a/test/act/act_drag_test.dart b/test/act/act_drag_test.dart index 33077c21..67f7ab35 100644 --- a/test/act/act_drag_test.dart +++ b/test/act/act_drag_test.dart @@ -2,11 +2,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; +import '../timeline/drag/drag_until_visible_test_widget.dart'; + void main() { group('Drag Events', () { testWidgets('Finds widget after dragging', (tester) async { + recordLiveTimeline(); await tester.pumpWidget( - const _ScrollableTestWidget(), + const DragUntilVisibleTestWidget(), ); final firstItem = spotText('Item at index: 3', exact: true)..existsOnce(); @@ -23,7 +26,7 @@ void main() { testWidgets('Throws TestFailure if not found', (tester) async { await tester.pumpWidget( - const _ScrollableTestWidget(), + const DragUntilVisibleTestWidget(), ); final firstItem = spotText('Item at index: 3', exact: true)..existsOnce(); @@ -51,49 +54,3 @@ void main() { }); }); } - -class _ScrollableTestWidget extends StatelessWidget { - const _ScrollableTestWidget(); - - Color getRandomColor(int index) { - return index.isEven ? Colors.red : Colors.blue; - } - - @override - Widget build(BuildContext context) { - final items = List.generate( - 30, - (index) => Container( - height: 100, - color: index.isEven ? Colors.red : Colors.blue, - child: Center(child: Text('Item at index: $index')), - ), - ); - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('Scrollable Test'), - ), - body: Center( - child: SizedBox( - height: 800, - child: Center( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 500, - maxHeight: 450, - ), - child: ListView.builder( - itemCount: items.length, - itemBuilder: (context, index) { - return items[index]; - }, - ), - ), - ), - ), - ), - ), - ); - } -} diff --git a/test/timeline/drag/drag_until_visible_test_widget.dart b/test/timeline/drag/drag_until_visible_test_widget.dart new file mode 100644 index 00000000..67c1f0a0 --- /dev/null +++ b/test/timeline/drag/drag_until_visible_test_widget.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +class DragUntilVisibleTestWidget extends StatelessWidget { + const DragUntilVisibleTestWidget({super.key}); + + Color getRandomColor(int index) { + return index.isEven ? Colors.red : Colors.blue; + } + + @override + Widget build(BuildContext context) { + final items = List.generate( + 30, + (index) => Container( + height: 100, + color: index.isEven ? Colors.red : Colors.blue, + child: Center(child: Text('Item at index: $index')), + ), + ); + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Scrollable Test'), + ), + body: Center( + child: SizedBox( + height: 800, + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500, + maxHeight: 450, + ), + child: ListView.builder( + itemCount: items.length, + itemBuilder: (context, index) { + return items[index]; + }, + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/test/timeline/drag/timeline_drag_test.dart b/test/timeline/drag/timeline_drag_test.dart new file mode 100644 index 00000000..55c0595e --- /dev/null +++ b/test/timeline/drag/timeline_drag_test.dart @@ -0,0 +1,439 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:dartx/dartx.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; +import 'package:spot/src/timeline/timeline.dart'; +import 'package:test_process/test_process.dart'; +import 'drag_until_visible_test_widget.dart'; + +final _firstItemSelector = spotText('Item at index: 3', exact: true); +final _secondItemSelector = spotText('Item at index: 27', exact: true); +final _addButtonSelector = spotIcon(Icons.add); +final _subtractButtonSelector = spotIcon(Icons.remove); +final _clearButtonSelector = spotIcon(Icons.clear); + +const _header = '==================== Timeline Event ===================='; +const _separator = '========================================================'; + +String _testAsString({ + required String title, + required TimelineMode timelineMode, + bool shouldFail = false, +}) { + final String methodForMode = () { + switch (timelineMode) { + case TimelineMode.live: + return 'recordLiveTimeline()'; + case TimelineMode.record: + return 'recordOnErrorTimeline()'; + case TimelineMode.off: + return 'stopRecordingTimeline()'; + } + }(); + + final widgetPart = + File('test/timeline/drag/drag_until_visible_test_widget.dart') + .readAsStringSync(); + return ''' +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; +import 'package:spot/src/timeline/timeline.dart';\n +$widgetPart\n +void main() async { + final addButtonSelector = spotIcon(Icons.add); + final subtractButtonSelector = spotIcon(Icons.remove); + testWidgets("$title", (WidgetTester tester) async { + $methodForMode; + await tester.pumpWidget(const DragUntilVisibleTestWidget()); + final firstItem = spotText('Item at index: 3', exact: true)..existsOnce(); + final secondItem = spotText('Item at index: 27', exact: true) + ..doesNotExist(); + await act.dragUntilVisible( + dragStart: firstItem, + dragTarget: secondItem, + maxIteration: 30, + moveStep: const Offset(0, -100), + ); + secondItem.existsOnce(); + }); +} +'''; +} + +void main() { + testWidgets('Drag Until Visible - Live timeline', (tester) async { + final output = await _captureConsoleOutput(() async { + recordLiveTimeline(); + await tester.pumpWidget(const DragUntilVisibleTestWidget()); + _firstItemSelector.existsOnce(); + _secondItemSelector.doesNotExist(); + await act.dragUntilVisible( + dragStart: _firstItemSelector, + dragTarget: _secondItemSelector, + maxIteration: 30, + moveStep: const Offset(0, -100), + ); + _secondItemSelector.existsOnce(); + // Notify that the timeline of this type is already recording. + recordLiveTimeline(); + }); + expect(output, contains('馃敶 - Now recording live timeline')); + expect( + output, + contains('Event: Starting to drag from'), + ); + expect( + output, + contains( + 'Target Widget with text with text "Item at index: 27" found after 23 drags. Total dragged offset:'), + ); + expect(output, contains('馃敶 - Already recording live timeline')); + }); + // testWidgets('Start with Timeline Mode off', (tester) async { + // final output = await _captureConsoleOutput(() async { + // stopRecordingTimeline(); + // await tester.pumpWidget(const TimelineTestWidget()); + // _addButtonSelector.existsOnce(); + // spotText('Counter: 3').existsOnce(); + // await act.tap(_addButtonSelector); + // spotText('Counter: 4').existsOnce(); + // await act.tap(_subtractButtonSelector); + // spotText('Counter: 3').existsOnce(); + // }); + // + // expect(output, contains('鈴革笌 - Timeline recording is off')); + // expect( + // output, + // isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), + // ); + // expect( + // output, + // isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), + // ); + // _testTimeLineContent(output: output, eventCount: 0); + // }); + // testWidgets('Turn timeline mode off during test', (tester) async { + // final output = await _captureConsoleOutput(() async { + // recordLiveTimeline(); + // await tester.pumpWidget( + // const TimelineTestWidget(), + // ); + // spotText('Counter: 3').existsOnce(); + // _addButtonSelector.existsOnce(); + // await act.tap(_addButtonSelector); + // spotText('Counter: 4').existsOnce(); + // await act.tap(_subtractButtonSelector); + // spotText('Counter: 3').existsOnce(); + // // Notify that the recording stopped + // stopRecordingTimeline(); + // await act.tap(_clearButtonSelector); + // spotText('Counter: 0').existsOnce(); + // // Notify that the recording is off + // stopRecordingTimeline(); + // }); + // expect(output, contains('馃敶 - Now recording live timeline')); + // expect(output, contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')); + // expect( + // output, + // contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), + // ); + // expect(output, contains('鈴革笌 - Timeline recording stopped')); + // // No further events were added to the timeline, including screenshots + // expect( + // output, + // isNot(contains('Tap ${_clearButtonSelector.toStringBreadcrumb()}')), + // ); + // _testTimeLineContent(output: output, eventCount: 2); + // expect(output, contains('鈴革笌 - Timeline recording is off')); + // }); + // + // group('Print on teardown', () { + // testWidgets('OnError timeline - without error', (tester) async { + // final output = await _captureConsoleOutput(() async { + // recordOnErrorTimeline(); + // await tester.pumpWidget(const DragUntilVisibleTestWidget()); + // _addButtonSelector.existsOnce(); + // spotText('Counter: 3').existsOnce(); + // await act.tap(_addButtonSelector); + // spotText('Counter: 4').existsOnce(); + // await act.tap(_subtractButtonSelector); + // spotText('Counter: 3').existsOnce(); + // // Notify that the timeline of this type is already recording. + // recordOnErrorTimeline(); + // }); + // expect(output, contains('馃敶 - Now recording error output timeline')); + // expect( + // output, + // isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), + // ); + // expect( + // output, + // isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), + // ); + // expect(output, contains('馃敶 - Already recording error output timeline')); + // _testTimeLineContent(output: output, eventCount: 0); + // }); + // + // test('OnError timeline - with error, prints timeline', () async { + // final tempDir = Directory.systemTemp.createTempSync(); + // final tempTestFile = File('${tempDir.path}/temp_test.dart'); + // await tempTestFile.writeAsString( + // _testAsString( + // title: 'OnError timeline with error', + // timelineMode: TimelineMode.record, + // shouldFail: true, + // ), + // ); + // + // final testProcess = + // await TestProcess.start('flutter', ['test', tempTestFile.path]); + // + // final stdoutBuffer = StringBuffer(); + // + // bool write = false; + // await for (final line in testProcess.stdoutStream()) { + // if (line.isEmpty) continue; + // if (line == 'Timeline') { + // write = true; + // } + // if (write) { + // stdoutBuffer.writeln(line); + // } + // } + // + // // Error happens + // await testProcess.shouldExit(1); + // tempDir.deleteSync(recursive: true); + // + // final stdout = stdoutBuffer.toString(); + // final timeline = stdout.split('\n'); + // + // expect(timeline.first, 'Timeline'); + // expect( + // timeline[1], + // _header, + // ); + // expect( + // timeline[2], + // 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', + // ); + // expect( + // timeline[3].startsWith('Caller: at main. file:///'), + // isTrue, + // ); + // expect( + // timeline[4].startsWith( + // 'Screenshot: file:///', + // ), + // isTrue, + // ); + // expect( + // timeline[5].startsWith( + // 'Timestamp:', + // ), + // isTrue, + // ); + // expect( + // timeline[6], + // _separator, + // ); + // final htmlLine = timeline + // .firstWhere((line) => line.startsWith('View time line here:')); + // expect( + // htmlLine.endsWith( + // 'timeline_onerror_timeline_with_error.html', + // ), + // isTrue, + // ); + // }); + // test('Live timeline - without error, prints HTML', () async { + // final tempDir = Directory.systemTemp.createTempSync(); + // final tempTestFile = File('${tempDir.path}/temp_test.dart'); + // await tempTestFile.writeAsString( + // _testAsString( + // title: 'Live timeline without error prints html', + // timelineMode: TimelineMode.live, + // ), + // ); + // + // final testProcess = + // await TestProcess.start('flutter', ['test', tempTestFile.path]); + // + // final stdoutBuffer = StringBuffer(); + // + // bool write = false; + // await for (final line in testProcess.stdoutStream()) { + // if (line.isEmpty) continue; + // + // if (!write) { + // if (line == _header) { + // write = true; + // } + // } + // + // if (write) { + // stdoutBuffer.writeln(line); + // } + // } + // + // // Error does not happen + // await testProcess.shouldExit(0); + // tempDir.deleteSync(recursive: true); + // + // final stdout = stdoutBuffer.toString(); + // final timeline = stdout.split('\n'); + // // Does not start with 'Timeline', this only happens on error + // expect(timeline.first, _header); + // expect( + // timeline.second, + // 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', + // ); + // expect( + // timeline[2].startsWith('Caller: at main. file:///'), + // isTrue, + // ); + // expect( + // timeline[3].startsWith( + // 'Screenshot: file:///', + // ), + // isTrue, + // ); + // expect( + // timeline[4].startsWith( + // 'Timestamp:', + // ), + // isTrue, + // ); + // expect( + // timeline[5], + // _separator, + // ); + // final htmlLine = timeline + // .firstWhere((line) => line.startsWith('View time line here:')); + // expect( + // htmlLine.endsWith( + // 'timeline_live_timeline_without_error_prints_html.html', + // ), + // isTrue, + // ); + // }); + // test('Live timeline - with error, no duplicates, prints HTML', () async { + // final tempDir = Directory.systemTemp.createTempSync(); + // final tempTestFile = File('${tempDir.path}/temp_test.dart'); + // await tempTestFile.writeAsString( + // _testAsString( + // title: 'Live timeline - with error, no duplicates, prints HTML', + // timelineMode: TimelineMode.live, + // shouldFail: true, + // ), + // ); + // + // final testProcess = + // await TestProcess.start('flutter', ['test', tempTestFile.path]); + // + // final stdoutBuffer = StringBuffer(); + // + // bool write = false; + // await for (final line in testProcess.stdoutStream()) { + // if (line.isEmpty) continue; + // + // if (!write) { + // if (line == _header) { + // write = true; + // } + // } + // + // if (write) { + // stdoutBuffer.writeln(line); + // } + // } + // + // // Error does not happen + // await testProcess.shouldExit(1); + // tempDir.deleteSync(recursive: true); + // + // final stdout = stdoutBuffer.toString(); + // final timeline = stdout.split('\n'); + // // Does not start with 'Timeline', this only happens on error + // expect(timeline.first, _header); + // expect( + // timeline.second, + // 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', + // ); + // expect( + // timeline[2].startsWith('Caller: at main. file:///'), + // isTrue, + // ); + // expect( + // timeline[3].startsWith( + // 'Screenshot: file:///', + // ), + // isTrue, + // ); + // expect( + // timeline[4].startsWith( + // 'Timestamp:', + // ), + // isTrue, + // ); + // expect( + // timeline[5], + // _separator, + // ); + // final htmlLine = timeline + // .firstWhere((line) => line.startsWith('View time line here:')); + // expect( + // htmlLine.endsWith( + // 'live_timeline_-_with_error,_no_duplicates,_prints_html.html', + // ), + // isTrue, + // ); + // }); + // }); +} + +void _testTimeLineContent({ + required String output, + required int eventCount, +}) { + expect( + RegExp(_header).allMatches(output).length, + eventCount, + ); + expect( + RegExp('Event: Tap Icon Widget with icon:').allMatches(output).length, + eventCount, + ); + expect( + RegExp('Caller: at main.. file:///').allMatches(output).length, + eventCount, + ); + expect( + RegExp('Screenshot: file:').allMatches(output).length, + eventCount, + ); + expect( + RegExp('Timestamp: ').allMatches(output).length, + eventCount, + ); +} + +Future _captureConsoleOutput( + Future Function() testFunction, +) async { + final StringBuffer buffer = StringBuffer(); + final ZoneSpecification spec = ZoneSpecification( + print: (self, parent, zone, line) { + buffer.writeln(line); + }, + ); + + await Zone.current.fork(specification: spec).run(() async { + await testFunction(); + }); + + return buffer.toString(); +} diff --git a/test/timeline/timeline_test.dart b/test/timeline/tap/timeline_tap_test.dart similarity index 98% rename from test/timeline/timeline_test.dart rename to test/timeline/tap/timeline_tap_test.dart index 28e5a7cc..2063bac5 100644 --- a/test/timeline/timeline_test.dart +++ b/test/timeline/tap/timeline_tap_test.dart @@ -7,7 +7,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import 'package:spot/src/timeline/timeline.dart'; import 'package:test_process/test_process.dart'; -import 'timeline_test_widget.dart'; +import 'timeline_tap_test_widget.dart'; final _addButtonSelector = spotIcon(Icons.add); final _subtractButtonSelector = spotIcon(Icons.remove); @@ -32,8 +32,8 @@ String _testAsString({ } }(); - final widgetPart = - File('test/timeline/timeline_test_widget.dart').readAsStringSync(); + final widgetPart = File('test/timeline/tap/timeline_tap_test_widget.dart') + .readAsStringSync(); return ''' import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; diff --git a/test/timeline/timeline_test_widget.dart b/test/timeline/tap/timeline_tap_test_widget.dart similarity index 100% rename from test/timeline/timeline_test_widget.dart rename to test/timeline/tap/timeline_tap_test_widget.dart From 05a08f024fbce875e16b90d1ad5244401fe61f5e Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Thu, 20 Jun 2024 03:47:09 +0200 Subject: [PATCH 076/119] Make captureConsoleOutput public --- test/util/capture_console_output.dart | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 test/util/capture_console_output.dart diff --git a/test/util/capture_console_output.dart b/test/util/capture_console_output.dart new file mode 100644 index 00000000..c34ee4d0 --- /dev/null +++ b/test/util/capture_console_output.dart @@ -0,0 +1,18 @@ +import 'dart:async'; + +Future captureConsoleOutput( + Future Function() testFunction, +) async { + final StringBuffer buffer = StringBuffer(); + final ZoneSpecification spec = ZoneSpecification( + print: (self, parent, zone, line) { + buffer.writeln(line); + }, + ); + + await Zone.current.fork(specification: spec).run(() async { + await testFunction(); + }); + + return buffer.toString(); +} From 5023c9bce85413307e2438623ce98d61e07dc1c5 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Thu, 20 Jun 2024 03:51:43 +0200 Subject: [PATCH 077/119] Add test "Start with Timeline Mode off" --- test/act/act_drag_test.dart | 1 - test/timeline/drag/timeline_drag_test.dart | 140 +++++++++------------ test/timeline/tap/timeline_tap_test.dart | 27 +--- 3 files changed, 67 insertions(+), 101 deletions(-) diff --git a/test/act/act_drag_test.dart b/test/act/act_drag_test.dart index 67f7ab35..303909a7 100644 --- a/test/act/act_drag_test.dart +++ b/test/act/act_drag_test.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; diff --git a/test/timeline/drag/timeline_drag_test.dart b/test/timeline/drag/timeline_drag_test.dart index 55c0595e..c51a3713 100644 --- a/test/timeline/drag/timeline_drag_test.dart +++ b/test/timeline/drag/timeline_drag_test.dart @@ -1,22 +1,16 @@ -import 'dart:async'; import 'dart:io'; -import 'package:dartx/dartx.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import 'package:spot/src/timeline/timeline.dart'; -import 'package:test_process/test_process.dart'; +import '../../util/capture_console_output.dart'; import 'drag_until_visible_test_widget.dart'; final _firstItemSelector = spotText('Item at index: 3', exact: true); final _secondItemSelector = spotText('Item at index: 27', exact: true); -final _addButtonSelector = spotIcon(Icons.add); -final _subtractButtonSelector = spotIcon(Icons.remove); -final _clearButtonSelector = spotIcon(Icons.clear); -const _header = '==================== Timeline Event ===================='; -const _separator = '========================================================'; +// const _header = '==================== Timeline Event ===================='; +// const _separator = '========================================================'; String _testAsString({ required String title, @@ -65,7 +59,7 @@ void main() async { void main() { testWidgets('Drag Until Visible - Live timeline', (tester) async { - final output = await _captureConsoleOutput(() async { + final output = await captureConsoleOutput(() async { recordLiveTimeline(); await tester.pumpWidget(const DragUntilVisibleTestWidget()); _firstItemSelector.existsOnce(); @@ -88,35 +82,42 @@ void main() { expect( output, contains( - 'Target Widget with text with text "Item at index: 27" found after 23 drags. Total dragged offset:'), + 'Target Widget with text with text "Item at index: 27" found after 23 drags. Total dragged offset:', + ), ); expect(output, contains('馃敶 - Already recording live timeline')); }); - // testWidgets('Start with Timeline Mode off', (tester) async { - // final output = await _captureConsoleOutput(() async { - // stopRecordingTimeline(); - // await tester.pumpWidget(const TimelineTestWidget()); - // _addButtonSelector.existsOnce(); - // spotText('Counter: 3').existsOnce(); - // await act.tap(_addButtonSelector); - // spotText('Counter: 4').existsOnce(); - // await act.tap(_subtractButtonSelector); - // spotText('Counter: 3').existsOnce(); - // }); - // - // expect(output, contains('鈴革笌 - Timeline recording is off')); - // expect( - // output, - // isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), - // ); - // expect( - // output, - // isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), - // ); - // _testTimeLineContent(output: output, eventCount: 0); - // }); + testWidgets('Start with Timeline Mode off', (tester) async { + final output = await captureConsoleOutput(() async { + stopRecordingTimeline(); + await tester.pumpWidget(const DragUntilVisibleTestWidget()); + _firstItemSelector.existsOnce(); + _secondItemSelector.doesNotExist(); + await act.dragUntilVisible( + dragStart: _firstItemSelector, + dragTarget: _secondItemSelector, + maxIteration: 30, + moveStep: const Offset(0, -100), + ); + _secondItemSelector.existsOnce(); + }); + + expect(output, contains('鈴革笌 - Timeline recording is off')); + expect( + output, + isNot(contains('Event: Starting to drag from')), + ); + expect( + output, + isNot( + contains( + 'Target Widget with text with text "Item at index: 27" found after 23 drags. Total dragged offset:', + ), + ), + ); + }); // testWidgets('Turn timeline mode off during test', (tester) async { - // final output = await _captureConsoleOutput(() async { + // final output = await captureConsoleOutput(() async { // recordLiveTimeline(); // await tester.pumpWidget( // const TimelineTestWidget(), @@ -152,7 +153,7 @@ void main() { // // group('Print on teardown', () { // testWidgets('OnError timeline - without error', (tester) async { - // final output = await _captureConsoleOutput(() async { + // final output = await captureConsoleOutput(() async { // recordOnErrorTimeline(); // await tester.pumpWidget(const DragUntilVisibleTestWidget()); // _addButtonSelector.existsOnce(); @@ -395,45 +396,28 @@ void main() { // }); } -void _testTimeLineContent({ - required String output, - required int eventCount, -}) { - expect( - RegExp(_header).allMatches(output).length, - eventCount, - ); - expect( - RegExp('Event: Tap Icon Widget with icon:').allMatches(output).length, - eventCount, - ); - expect( - RegExp('Caller: at main.. file:///').allMatches(output).length, - eventCount, - ); - expect( - RegExp('Screenshot: file:').allMatches(output).length, - eventCount, - ); - expect( - RegExp('Timestamp: ').allMatches(output).length, - eventCount, - ); -} - -Future _captureConsoleOutput( - Future Function() testFunction, -) async { - final StringBuffer buffer = StringBuffer(); - final ZoneSpecification spec = ZoneSpecification( - print: (self, parent, zone, line) { - buffer.writeln(line); - }, - ); - - await Zone.current.fork(specification: spec).run(() async { - await testFunction(); - }); - - return buffer.toString(); -} +// void _testTimeLineContent({ +// required String output, +// required int eventCount, +// }) { +// expect( +// RegExp(_header).allMatches(output).length, +// eventCount, +// ); +// expect( +// RegExp('Event: Tap Icon Widget with icon:').allMatches(output).length, +// eventCount, +// ); +// expect( +// RegExp('Caller: at main.. file:///').allMatches(output).length, +// eventCount, +// ); +// expect( +// RegExp('Screenshot: file:').allMatches(output).length, +// eventCount, +// ); +// expect( +// RegExp('Timestamp: ').allMatches(output).length, +// eventCount, +// ); +// } diff --git a/test/timeline/tap/timeline_tap_test.dart b/test/timeline/tap/timeline_tap_test.dart index 2063bac5..f3b51e1b 100644 --- a/test/timeline/tap/timeline_tap_test.dart +++ b/test/timeline/tap/timeline_tap_test.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:io'; import 'package:dartx/dartx.dart'; @@ -7,6 +6,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import 'package:spot/src/timeline/timeline.dart'; import 'package:test_process/test_process.dart'; +import '../../util/capture_console_output.dart'; import 'timeline_tap_test_widget.dart'; final _addButtonSelector = spotIcon(Icons.add); @@ -59,7 +59,7 @@ void main() async { void main() { testWidgets('Live timeline', (tester) async { - final output = await _captureConsoleOutput(() async { + final output = await captureConsoleOutput(() async { recordLiveTimeline(); await tester.pumpWidget(const TimelineTestWidget()); _addButtonSelector.existsOnce(); @@ -84,7 +84,7 @@ void main() { _testTimeLineContent(output: output, eventCount: 2); }); testWidgets('Start with Timeline Mode off', (tester) async { - final output = await _captureConsoleOutput(() async { + final output = await captureConsoleOutput(() async { stopRecordingTimeline(); await tester.pumpWidget(const TimelineTestWidget()); _addButtonSelector.existsOnce(); @@ -107,7 +107,7 @@ void main() { _testTimeLineContent(output: output, eventCount: 0); }); testWidgets('Turn timeline mode off during test', (tester) async { - final output = await _captureConsoleOutput(() async { + final output = await captureConsoleOutput(() async { recordLiveTimeline(); await tester.pumpWidget( const TimelineTestWidget(), @@ -143,7 +143,7 @@ void main() { group('Print on teardown', () { testWidgets('OnError timeline - without error', (tester) async { - final output = await _captureConsoleOutput(() async { + final output = await captureConsoleOutput(() async { recordOnErrorTimeline(); await tester.pumpWidget(const TimelineTestWidget()); _addButtonSelector.existsOnce(); @@ -411,20 +411,3 @@ void _testTimeLineContent({ eventCount, ); } - -Future _captureConsoleOutput( - Future Function() testFunction, -) async { - final StringBuffer buffer = StringBuffer(); - final ZoneSpecification spec = ZoneSpecification( - print: (self, parent, zone, line) { - buffer.writeln(line); - }, - ); - - await Zone.current.fork(specification: spec).run(() async { - await testFunction(); - }); - - return buffer.toString(); -} From ff47986443967f2c5f2a275fcdd5afd0dc473b20 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Thu, 20 Jun 2024 17:09:18 +0200 Subject: [PATCH 078/119] Add test drag: OnError timeline - without error --- lib/src/act/act.dart | 14 +- test/timeline/drag/timeline_drag_test.dart | 784 ++++++++++----------- 2 files changed, 393 insertions(+), 405 deletions(-) diff --git a/lib/src/act/act.dart b/lib/src/act/act.dart index e049e5c0..e4852176 100644 --- a/lib/src/act/act.dart +++ b/lib/src/act/act.dart @@ -205,25 +205,25 @@ class Act { name: 'Starting to drag from $dragPosition towards $targetName.', ); - int iterations = 0; - while (iterations < maxIteration && !isVisible) { + int dragCount = 0; + while (dragCount < maxIteration && !isVisible) { await gestures.drag(dragPosition, moveStep); await binding.pump(duration); - iterations++; + dragCount++; isVisible = isTargetVisible(); } - final totalDragged = moveStep * iterations.toDouble(); - final resultString = isVisible ? '' : 'not'; + final totalDragged = moveStep * dragCount.toDouble(); + final resultString = isVisible ? 'found' : 'not found'; final message = - "Target $targetName $resultString found after $iterations drags. " + "Target $targetName $resultString after $dragCount drags. " "Total dragged offset: $totalDragged"; await addDragEvent(name: message); if (!isVisible) { throw TestFailure( - "$targetName is not visible after dragging $iterations times and a total dragged offset of $totalDragged.", + "$targetName is not visible after dragging $dragCount times and a total dragged offset of $totalDragged.", ); } }); diff --git a/test/timeline/drag/timeline_drag_test.dart b/test/timeline/drag/timeline_drag_test.dart index c51a3713..65427857 100644 --- a/test/timeline/drag/timeline_drag_test.dart +++ b/test/timeline/drag/timeline_drag_test.dart @@ -1,423 +1,411 @@ -import 'dart:io'; - +import 'package:dartx/dartx.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; -import 'package:spot/src/timeline/timeline.dart'; +// import 'package:spot/src/timeline/timeline.dart'; +// import 'package:test_process/test_process.dart'; import '../../util/capture_console_output.dart'; import 'drag_until_visible_test_widget.dart'; final _firstItemSelector = spotText('Item at index: 3', exact: true); final _secondItemSelector = spotText('Item at index: 27', exact: true); -// const _header = '==================== Timeline Event ===================='; +const _header = '==================== Timeline Event ===================='; // const _separator = '========================================================'; -String _testAsString({ - required String title, - required TimelineMode timelineMode, - bool shouldFail = false, -}) { - final String methodForMode = () { - switch (timelineMode) { - case TimelineMode.live: - return 'recordLiveTimeline()'; - case TimelineMode.record: - return 'recordOnErrorTimeline()'; - case TimelineMode.off: - return 'stopRecordingTimeline()'; - } - }(); +// String _testAsString({ +// required String title, +// required TimelineMode timelineMode, +// bool shouldFail = false, +// }) { +// final String methodForMode = () { +// switch (timelineMode) { +// case TimelineMode.live: +// return 'recordLiveTimeline()'; +// case TimelineMode.record: +// return 'recordOnErrorTimeline()'; +// case TimelineMode.off: +// return 'stopRecordingTimeline()'; +// } +// }(); +// +// final widgetPart = +// File('test/timeline/drag/drag_until_visible_test_widget.dart') +// .readAsStringSync(); +// return ''' +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:spot/spot.dart'; +// import 'package:spot/src/timeline/timeline.dart';\n +// $widgetPart\n +// void main() async { +// final addButtonSelector = spotIcon(Icons.add); +// final subtractButtonSelector = spotIcon(Icons.remove); +// testWidgets("$title", (WidgetTester tester) async { +// $methodForMode; +// await tester.pumpWidget(const DragUntilVisibleTestWidget()); +// final firstItem = spotText('Item at index: 3', exact: true)..existsOnce(); +// final secondItem = spotText('Item at index: 27', exact: true) +// ..doesNotExist(); +// await act.dragUntilVisible( +// dragStart: firstItem, +// dragTarget: secondItem, +// maxIteration: 30, +// moveStep: const Offset(0, -100), +// ); +// secondItem.existsOnce(); +// }); +// } +// '''; +// } - final widgetPart = - File('test/timeline/drag/drag_until_visible_test_widget.dart') - .readAsStringSync(); - return ''' -import 'package:flutter_test/flutter_test.dart'; -import 'package:spot/spot.dart'; -import 'package:spot/src/timeline/timeline.dart';\n -$widgetPart\n -void main() async { - final addButtonSelector = spotIcon(Icons.add); - final subtractButtonSelector = spotIcon(Icons.remove); - testWidgets("$title", (WidgetTester tester) async { - $methodForMode; - await tester.pumpWidget(const DragUntilVisibleTestWidget()); - final firstItem = spotText('Item at index: 3', exact: true)..existsOnce(); - final secondItem = spotText('Item at index: 27', exact: true) - ..doesNotExist(); - await act.dragUntilVisible( - dragStart: firstItem, - dragTarget: secondItem, - maxIteration: 30, - moveStep: const Offset(0, -100), +void main() { + group('Drag Timeline Test', () { + testWidgets('Drag Until Visible - Live timeline', (tester) async { + final output = await captureConsoleOutput(() async { + recordLiveTimeline(); + await tester.pumpWidget(const DragUntilVisibleTestWidget()); + _firstItemSelector.existsOnce(); + _secondItemSelector.doesNotExist(); + await act.dragUntilVisible( + dragStart: _firstItemSelector, + dragTarget: _secondItemSelector, + maxIteration: 30, + moveStep: const Offset(0, -100), + ); + _secondItemSelector.existsOnce(); + // Notify that the timeline of this type is already recording. + recordLiveTimeline(); + }); + expect(output, contains('馃敶 - Now recording live timeline')); + _testTimeLineContent( + output: output, + dragEvents: 2, + targetSelector: _secondItemSelector, + totalExpectedOffset: const Offset(0, -2300), + drags: 23, ); - secondItem.existsOnce(); - }); -} -'''; -} + expect(output, contains('馃敶 - Already recording live timeline')); + }); + testWidgets('Start with Timeline Mode off', (tester) async { + final output = await captureConsoleOutput(() async { + stopRecordingTimeline(); + await tester.pumpWidget(const DragUntilVisibleTestWidget()); + _firstItemSelector.existsOnce(); + _secondItemSelector.doesNotExist(); + await act.dragUntilVisible( + dragStart: _firstItemSelector, + dragTarget: _secondItemSelector, + maxIteration: 30, + moveStep: const Offset(0, -100), + ); + _secondItemSelector.existsOnce(); + }); + final splitted = output.split('\n')..removeWhere((line) => line.isEmpty); + const expectedOutput = '鈴革笌 - Timeline recording is off'; + expect(splitted.length, 1); + expect(splitted.first, expectedOutput); + }); + testWidgets('Turn timeline mode off during test', (tester) async { + final output = await captureConsoleOutput(() async { + recordLiveTimeline(); + await tester.pumpWidget(const DragUntilVisibleTestWidget()); + _firstItemSelector.existsOnce(); + _secondItemSelector.doesNotExist(); + await act.dragUntilVisible( + dragStart: _firstItemSelector, + dragTarget: _secondItemSelector, + maxIteration: 30, + moveStep: const Offset(0, -100), + ); + _secondItemSelector.existsOnce(); -void main() { - testWidgets('Drag Until Visible - Live timeline', (tester) async { - final output = await captureConsoleOutput(() async { - recordLiveTimeline(); - await tester.pumpWidget(const DragUntilVisibleTestWidget()); - _firstItemSelector.existsOnce(); - _secondItemSelector.doesNotExist(); - await act.dragUntilVisible( - dragStart: _firstItemSelector, - dragTarget: _secondItemSelector, - maxIteration: 30, - moveStep: const Offset(0, -100), + // Notify that the recording is off + stopRecordingTimeline(); + stopRecordingTimeline(); + }); + expect(output, contains('馃敶 - Now recording live timeline')); + _testTimeLineContent( + output: output, + dragEvents: 2, + targetSelector: _secondItemSelector, + totalExpectedOffset: const Offset(0, -2300), + drags: 23, ); - _secondItemSelector.existsOnce(); - // Notify that the timeline of this type is already recording. - recordLiveTimeline(); + + expect(output, contains('鈴革笌 - Timeline recording stopped')); + expect(output, contains('鈴革笌 - Timeline recording is off')); + }); + + group('Print on teardown', () { + testWidgets('OnError timeline - without error', (tester) async { + final output = await captureConsoleOutput(() async { + recordOnErrorTimeline(); + await tester.pumpWidget(const DragUntilVisibleTestWidget()); + _firstItemSelector.existsOnce(); + _secondItemSelector.doesNotExist(); + await act.dragUntilVisible( + dragStart: _firstItemSelector, + dragTarget: _secondItemSelector, + maxIteration: 30, + moveStep: const Offset(0, -100), + ); + _secondItemSelector.existsOnce(); + recordOnErrorTimeline(); + }); + final splitted = output.split('\n') + ..removeWhere((line) => line.isEmpty); + expect(splitted.length, 2); + expect(splitted.first, '馃敶 - Now recording error output timeline'); + expect(splitted.second, '馃敶 - Already recording error output timeline'); + }); + // test('OnError timeline - with error, prints timeline', () async { + // final tempDir = Directory.systemTemp.createTempSync(); + // final tempTestFile = File('${tempDir.path}/temp_test.dart'); + // await tempTestFile.writeAsString( + // _testAsString( + // title: 'OnError timeline with error', + // timelineMode: TimelineMode.record, + // shouldFail: true, + // ), + // ); + // final testProcess = + // await TestProcess.start('flutter', ['test', tempTestFile.path]); + // final stdoutBuffer = StringBuffer(); + // bool write = false; + // await for (final line in testProcess.stdoutStream()) { + // if (line.isEmpty) continue; + // if (line == 'Timeline') { + // write = true; + // } + // if (write) { + // stdoutBuffer.writeln(line); + // } + // // Error happens + // await testProcess.shouldExit(1); + // tempDir.deleteSync(recursive: true); + // final stdout = stdoutBuffer.toString(); + // final timeline = stdout.split('\n'); + // + // expect(timeline.first, 'Timeline'); + // expect( + // timeline[1], + // _header, + // ); + // expect( + // timeline[2], + // 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', + // ); + // expect( + // timeline[3].startsWith('Caller: at main. file:///'), + // isTrue, + // ); + // expect( + // timeline[4].startsWith( + // 'Screenshot: file:///', + // ), + // isTrue, + // ); + // expect( + // timeline[5].startsWith( + // 'Timestamp:', + // ), + // isTrue, + // ); + // expect( + // timeline[6], + // _separator, + // ); + // final htmlLine = timeline + // .firstWhere((line) => line.startsWith('View time line here:')); + // expect( + // htmlLine.endsWith( + // 'timeline_onerror_timeline_with_error.html', + // ), + // isTrue, + // ); + // } + // }); + // test('Live timeline - without error, prints HTML', () async { + // final tempDir = Directory.systemTemp.createTempSync(); + // final tempTestFile = File('${tempDir.path}/temp_test.dart'); + // await tempTestFile.writeAsString( + // _testAsString( + // title: 'Live timeline without error prints html', + // timelineMode: TimelineMode.live, + // ), + // ); + // final testProcess = + // await TestProcess.start('flutter', ['test', tempTestFile.path]); + // final stdoutBuffer = StringBuffer(); + // bool write = false; + // await for (final line in testProcess.stdoutStream()) { + // if (line.isEmpty) continue; + // if (!write) { + // if (line == _header) { + // write = true; + // } + // if (write) { + // stdoutBuffer.writeln(line); + // } // Error does not happen + // } + // await testProcess.shouldExit(0); + // tempDir.deleteSync(recursive: true); + // final stdout = stdoutBuffer.toString(); + // final timeline = stdout.split('\n'); + // // Does not start with 'Timeline', this only happens on error + // expect(timeline.first, _header); + // expect( + // timeline.second, + // 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', + // ); + // expect( + // timeline[2].startsWith('Caller: at main. file:///'), + // isTrue, + // ); + // expect( + // timeline[3].startsWith( + // 'Screenshot: file:///', + // ), + // isTrue, + // ); + // expect( + // timeline[4].startsWith( + // 'Timestamp:', + // ), + // isTrue, + // ); + // expect( + // timeline[5], + // _separator, + // ); + // final htmlLine = timeline + // .firstWhere((line) => line.startsWith('View time line here:')); + // expect( + // htmlLine.endsWith( + // 'timeline_live_timeline_without_error_prints_html.html', + // ), + // isTrue, + // ); + // } + // }); + // test('Live timeline - with error, no duplicates, prints HTML', () async { + // final tempDir = Directory.systemTemp.createTempSync(); + // final tempTestFile = File('${tempDir.path}/temp_test.dart'); + // await tempTestFile.writeAsString( + // _testAsString( + // title: 'Live timeline - with error, no duplicates, prints HTML', + // timelineMode: TimelineMode.live, + // shouldFail: true, + // ), + // ); + // final testProcess = + // await TestProcess.start('flutter', ['test', tempTestFile.path]); + // final stdoutBuffer = StringBuffer(); + // bool write = false; + // await for (final line in testProcess.stdoutStream()) { + // if (line.isEmpty) continue; + // if (!write) { + // if (line == _header) { + // write = true; + // } + // if (write) { + // stdoutBuffer.writeln(line); + // } // Error does not happen + // await testProcess.shouldExit(1); + // tempDir.deleteSync(recursive: true); + // final stdout = stdoutBuffer.toString(); + // final timeline = stdout.split('\n'); + // // Does not start with 'Timeline', this only happens on error + // expect(timeline.first, _header); + // expect( + // timeline.second, + // 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', + // ); + // expect( + // timeline[2].startsWith('Caller: at main. file:///'), + // isTrue, + // ); + // expect( + // timeline[3].startsWith( + // 'Screenshot: file:///', + // ), + // isTrue, + // ); + // expect( + // timeline[4].startsWith( + // 'Timestamp:', + // ), + // isTrue, + // ); + // expect( + // timeline[5], + // _separator, + // ); + // final htmlLine = timeline + // .firstWhere((line) => line.startsWith('View time line here:')); + // expect( + // htmlLine.endsWith( + // 'live_timeline_-_with_error,_no_duplicates,_prints_html.html', + // ), + // isTrue, + // ); + // } + // } + // }); }); - expect(output, contains('馃敶 - Now recording live timeline')); + }); +} + +void _testTimeLineContent({ + required String output, + required int dragEvents, + bool findsWidget = true, + required WidgetSelector targetSelector, + required Offset totalExpectedOffset, + int? drags, +}) { + final eventLines = + output.split('\n').where((line) => line.startsWith('Event:')); + final startEvent = _replaceOffsetWithDxDy(eventLines.first); + final endEvent = + 'Event: Target ${targetSelector.toStringBreadcrumb()} ${findsWidget ? 'found' : ''} after $drags drags. Total dragged offset: $totalExpectedOffset'; + + for (int i = 0; i < 2; i++) { expect( - output, - contains('Event: Starting to drag from'), + RegExp(_header).allMatches(output).length, + dragEvents, ); + if (i == 0) { + expect( + startEvent, + 'Event: Starting to drag from Offset(dx,dy) towards ${targetSelector.toStringBreadcrumb()}.', + ); + } else { + expect( + eventLines.last, + endEvent, + ); + } expect( - output, - contains( - 'Target Widget with text with text "Item at index: 27" found after 23 drags. Total dragged offset:', - ), + RegExp('Caller: at main... file:///') + .allMatches(output) + .length, + dragEvents, ); - expect(output, contains('馃敶 - Already recording live timeline')); - }); - testWidgets('Start with Timeline Mode off', (tester) async { - final output = await captureConsoleOutput(() async { - stopRecordingTimeline(); - await tester.pumpWidget(const DragUntilVisibleTestWidget()); - _firstItemSelector.existsOnce(); - _secondItemSelector.doesNotExist(); - await act.dragUntilVisible( - dragStart: _firstItemSelector, - dragTarget: _secondItemSelector, - maxIteration: 30, - moveStep: const Offset(0, -100), - ); - _secondItemSelector.existsOnce(); - }); - - expect(output, contains('鈴革笌 - Timeline recording is off')); expect( - output, - isNot(contains('Event: Starting to drag from')), + RegExp('Screenshot: file:').allMatches(output).length, + dragEvents, ); expect( - output, - isNot( - contains( - 'Target Widget with text with text "Item at index: 27" found after 23 drags. Total dragged offset:', - ), - ), + RegExp('Timestamp: ').allMatches(output).length, + dragEvents, ); - }); - // testWidgets('Turn timeline mode off during test', (tester) async { - // final output = await captureConsoleOutput(() async { - // recordLiveTimeline(); - // await tester.pumpWidget( - // const TimelineTestWidget(), - // ); - // spotText('Counter: 3').existsOnce(); - // _addButtonSelector.existsOnce(); - // await act.tap(_addButtonSelector); - // spotText('Counter: 4').existsOnce(); - // await act.tap(_subtractButtonSelector); - // spotText('Counter: 3').existsOnce(); - // // Notify that the recording stopped - // stopRecordingTimeline(); - // await act.tap(_clearButtonSelector); - // spotText('Counter: 0').existsOnce(); - // // Notify that the recording is off - // stopRecordingTimeline(); - // }); - // expect(output, contains('馃敶 - Now recording live timeline')); - // expect(output, contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')); - // expect( - // output, - // contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), - // ); - // expect(output, contains('鈴革笌 - Timeline recording stopped')); - // // No further events were added to the timeline, including screenshots - // expect( - // output, - // isNot(contains('Tap ${_clearButtonSelector.toStringBreadcrumb()}')), - // ); - // _testTimeLineContent(output: output, eventCount: 2); - // expect(output, contains('鈴革笌 - Timeline recording is off')); - // }); - // - // group('Print on teardown', () { - // testWidgets('OnError timeline - without error', (tester) async { - // final output = await captureConsoleOutput(() async { - // recordOnErrorTimeline(); - // await tester.pumpWidget(const DragUntilVisibleTestWidget()); - // _addButtonSelector.existsOnce(); - // spotText('Counter: 3').existsOnce(); - // await act.tap(_addButtonSelector); - // spotText('Counter: 4').existsOnce(); - // await act.tap(_subtractButtonSelector); - // spotText('Counter: 3').existsOnce(); - // // Notify that the timeline of this type is already recording. - // recordOnErrorTimeline(); - // }); - // expect(output, contains('馃敶 - Now recording error output timeline')); - // expect( - // output, - // isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), - // ); - // expect( - // output, - // isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), - // ); - // expect(output, contains('馃敶 - Already recording error output timeline')); - // _testTimeLineContent(output: output, eventCount: 0); - // }); - // - // test('OnError timeline - with error, prints timeline', () async { - // final tempDir = Directory.systemTemp.createTempSync(); - // final tempTestFile = File('${tempDir.path}/temp_test.dart'); - // await tempTestFile.writeAsString( - // _testAsString( - // title: 'OnError timeline with error', - // timelineMode: TimelineMode.record, - // shouldFail: true, - // ), - // ); - // - // final testProcess = - // await TestProcess.start('flutter', ['test', tempTestFile.path]); - // - // final stdoutBuffer = StringBuffer(); - // - // bool write = false; - // await for (final line in testProcess.stdoutStream()) { - // if (line.isEmpty) continue; - // if (line == 'Timeline') { - // write = true; - // } - // if (write) { - // stdoutBuffer.writeln(line); - // } - // } - // - // // Error happens - // await testProcess.shouldExit(1); - // tempDir.deleteSync(recursive: true); - // - // final stdout = stdoutBuffer.toString(); - // final timeline = stdout.split('\n'); - // - // expect(timeline.first, 'Timeline'); - // expect( - // timeline[1], - // _header, - // ); - // expect( - // timeline[2], - // 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', - // ); - // expect( - // timeline[3].startsWith('Caller: at main. file:///'), - // isTrue, - // ); - // expect( - // timeline[4].startsWith( - // 'Screenshot: file:///', - // ), - // isTrue, - // ); - // expect( - // timeline[5].startsWith( - // 'Timestamp:', - // ), - // isTrue, - // ); - // expect( - // timeline[6], - // _separator, - // ); - // final htmlLine = timeline - // .firstWhere((line) => line.startsWith('View time line here:')); - // expect( - // htmlLine.endsWith( - // 'timeline_onerror_timeline_with_error.html', - // ), - // isTrue, - // ); - // }); - // test('Live timeline - without error, prints HTML', () async { - // final tempDir = Directory.systemTemp.createTempSync(); - // final tempTestFile = File('${tempDir.path}/temp_test.dart'); - // await tempTestFile.writeAsString( - // _testAsString( - // title: 'Live timeline without error prints html', - // timelineMode: TimelineMode.live, - // ), - // ); - // - // final testProcess = - // await TestProcess.start('flutter', ['test', tempTestFile.path]); - // - // final stdoutBuffer = StringBuffer(); - // - // bool write = false; - // await for (final line in testProcess.stdoutStream()) { - // if (line.isEmpty) continue; - // - // if (!write) { - // if (line == _header) { - // write = true; - // } - // } - // - // if (write) { - // stdoutBuffer.writeln(line); - // } - // } - // - // // Error does not happen - // await testProcess.shouldExit(0); - // tempDir.deleteSync(recursive: true); - // - // final stdout = stdoutBuffer.toString(); - // final timeline = stdout.split('\n'); - // // Does not start with 'Timeline', this only happens on error - // expect(timeline.first, _header); - // expect( - // timeline.second, - // 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', - // ); - // expect( - // timeline[2].startsWith('Caller: at main. file:///'), - // isTrue, - // ); - // expect( - // timeline[3].startsWith( - // 'Screenshot: file:///', - // ), - // isTrue, - // ); - // expect( - // timeline[4].startsWith( - // 'Timestamp:', - // ), - // isTrue, - // ); - // expect( - // timeline[5], - // _separator, - // ); - // final htmlLine = timeline - // .firstWhere((line) => line.startsWith('View time line here:')); - // expect( - // htmlLine.endsWith( - // 'timeline_live_timeline_without_error_prints_html.html', - // ), - // isTrue, - // ); - // }); - // test('Live timeline - with error, no duplicates, prints HTML', () async { - // final tempDir = Directory.systemTemp.createTempSync(); - // final tempTestFile = File('${tempDir.path}/temp_test.dart'); - // await tempTestFile.writeAsString( - // _testAsString( - // title: 'Live timeline - with error, no duplicates, prints HTML', - // timelineMode: TimelineMode.live, - // shouldFail: true, - // ), - // ); - // - // final testProcess = - // await TestProcess.start('flutter', ['test', tempTestFile.path]); - // - // final stdoutBuffer = StringBuffer(); - // - // bool write = false; - // await for (final line in testProcess.stdoutStream()) { - // if (line.isEmpty) continue; - // - // if (!write) { - // if (line == _header) { - // write = true; - // } - // } - // - // if (write) { - // stdoutBuffer.writeln(line); - // } - // } - // - // // Error does not happen - // await testProcess.shouldExit(1); - // tempDir.deleteSync(recursive: true); - // - // final stdout = stdoutBuffer.toString(); - // final timeline = stdout.split('\n'); - // // Does not start with 'Timeline', this only happens on error - // expect(timeline.first, _header); - // expect( - // timeline.second, - // 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', - // ); - // expect( - // timeline[2].startsWith('Caller: at main. file:///'), - // isTrue, - // ); - // expect( - // timeline[3].startsWith( - // 'Screenshot: file:///', - // ), - // isTrue, - // ); - // expect( - // timeline[4].startsWith( - // 'Timestamp:', - // ), - // isTrue, - // ); - // expect( - // timeline[5], - // _separator, - // ); - // final htmlLine = timeline - // .firstWhere((line) => line.startsWith('View time line here:')); - // expect( - // htmlLine.endsWith( - // 'live_timeline_-_with_error,_no_duplicates,_prints_html.html', - // ), - // isTrue, - // ); - // }); - // }); + } } -// void _testTimeLineContent({ -// required String output, -// required int eventCount, -// }) { -// expect( -// RegExp(_header).allMatches(output).length, -// eventCount, -// ); -// expect( -// RegExp('Event: Tap Icon Widget with icon:').allMatches(output).length, -// eventCount, -// ); -// expect( -// RegExp('Caller: at main.. file:///').allMatches(output).length, -// eventCount, -// ); -// expect( -// RegExp('Screenshot: file:').allMatches(output).length, -// eventCount, -// ); -// expect( -// RegExp('Timestamp: ').allMatches(output).length, -// eventCount, -// ); -// } +String _replaceOffsetWithDxDy(String originalString) { + // Regular expression to match 'Offset(any values)' + final RegExp offsetPattern = RegExp(r'Offset\([^)]*\)'); + + // Replace all matches with 'Offset(dx,dy)' + return originalString.replaceAll(offsetPattern, 'Offset(dx,dy)'); +} From bc928e3c90beb09e6d5878a01d051b3a8225750f Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 21 Jun 2024 02:12:21 +0200 Subject: [PATCH 079/119] Add test OnError timeline - with error, prints timeline --- test/timeline/drag/timeline_drag_test.dart | 235 ++++++++++----------- test/timeline/tap/timeline_tap_test.dart | 15 +- 2 files changed, 118 insertions(+), 132 deletions(-) diff --git a/test/timeline/drag/timeline_drag_test.dart b/test/timeline/drag/timeline_drag_test.dart index 65427857..1f28a445 100644 --- a/test/timeline/drag/timeline_drag_test.dart +++ b/test/timeline/drag/timeline_drag_test.dart @@ -1,8 +1,10 @@ +import 'dart:io'; + import 'package:dartx/dartx.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; -// import 'package:spot/src/timeline/timeline.dart'; -// import 'package:test_process/test_process.dart'; +import 'package:spot/src/timeline/timeline.dart'; +import 'package:test_process/test_process.dart'; import '../../util/capture_console_output.dart'; import 'drag_until_visible_test_widget.dart'; @@ -12,50 +14,48 @@ final _secondItemSelector = spotText('Item at index: 27', exact: true); const _header = '==================== Timeline Event ===================='; // const _separator = '========================================================'; -// String _testAsString({ -// required String title, -// required TimelineMode timelineMode, -// bool shouldFail = false, -// }) { -// final String methodForMode = () { -// switch (timelineMode) { -// case TimelineMode.live: -// return 'recordLiveTimeline()'; -// case TimelineMode.record: -// return 'recordOnErrorTimeline()'; -// case TimelineMode.off: -// return 'stopRecordingTimeline()'; -// } -// }(); -// -// final widgetPart = -// File('test/timeline/drag/drag_until_visible_test_widget.dart') -// .readAsStringSync(); -// return ''' -// import 'package:flutter_test/flutter_test.dart'; -// import 'package:spot/spot.dart'; -// import 'package:spot/src/timeline/timeline.dart';\n -// $widgetPart\n -// void main() async { -// final addButtonSelector = spotIcon(Icons.add); -// final subtractButtonSelector = spotIcon(Icons.remove); -// testWidgets("$title", (WidgetTester tester) async { -// $methodForMode; -// await tester.pumpWidget(const DragUntilVisibleTestWidget()); -// final firstItem = spotText('Item at index: 3', exact: true)..existsOnce(); -// final secondItem = spotText('Item at index: 27', exact: true) -// ..doesNotExist(); -// await act.dragUntilVisible( -// dragStart: firstItem, -// dragTarget: secondItem, -// maxIteration: 30, -// moveStep: const Offset(0, -100), -// ); -// secondItem.existsOnce(); -// }); -// } -// '''; -// } +String _testAsString({ + required String title, + required TimelineMode timelineMode, + bool shouldFail = false, +}) { + final String methodForMode = () { + switch (timelineMode) { + case TimelineMode.live: + return 'recordLiveTimeline()'; + case TimelineMode.record: + return 'recordOnErrorTimeline()'; + case TimelineMode.off: + return 'stopRecordingTimeline()'; + } + }(); + + final widgetPart = + File('test/timeline/drag/drag_until_visible_test_widget.dart') + .readAsStringSync(); + return ''' +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; +import 'package:spot/src/timeline/timeline.dart';\n +$widgetPart\n +void main() async { + testWidgets("$title", (WidgetTester tester) async { + $methodForMode; + await tester.pumpWidget(const DragUntilVisibleTestWidget()); + final firstItem = spotText('Item at index: 3', exact: true)..existsOnce(); + final secondItem = spotText('Item at index: 27', exact: true) + ..doesNotExist(); + await act.dragUntilVisible( + dragStart: firstItem, + dragTarget: secondItem, + maxIteration: ${shouldFail ? '10' : '30'}, + moveStep: const Offset(0, -100), + ); + secondItem.existsOnce(); + }); +} +'''; +} void main() { group('Drag Timeline Test', () { @@ -78,8 +78,6 @@ void main() { expect(output, contains('馃敶 - Now recording live timeline')); _testTimeLineContent( output: output, - dragEvents: 2, - targetSelector: _secondItemSelector, totalExpectedOffset: const Offset(0, -2300), drags: 23, ); @@ -125,8 +123,6 @@ void main() { expect(output, contains('馃敶 - Now recording live timeline')); _testTimeLineContent( output: output, - dragEvents: 2, - targetSelector: _secondItemSelector, totalExpectedOffset: const Offset(0, -2300), drags: 23, ); @@ -157,73 +153,58 @@ void main() { expect(splitted.first, '馃敶 - Now recording error output timeline'); expect(splitted.second, '馃敶 - Already recording error output timeline'); }); - // test('OnError timeline - with error, prints timeline', () async { - // final tempDir = Directory.systemTemp.createTempSync(); - // final tempTestFile = File('${tempDir.path}/temp_test.dart'); - // await tempTestFile.writeAsString( - // _testAsString( - // title: 'OnError timeline with error', - // timelineMode: TimelineMode.record, - // shouldFail: true, - // ), - // ); - // final testProcess = - // await TestProcess.start('flutter', ['test', tempTestFile.path]); - // final stdoutBuffer = StringBuffer(); - // bool write = false; - // await for (final line in testProcess.stdoutStream()) { - // if (line.isEmpty) continue; - // if (line == 'Timeline') { - // write = true; - // } - // if (write) { - // stdoutBuffer.writeln(line); - // } - // // Error happens - // await testProcess.shouldExit(1); - // tempDir.deleteSync(recursive: true); - // final stdout = stdoutBuffer.toString(); - // final timeline = stdout.split('\n'); - // - // expect(timeline.first, 'Timeline'); - // expect( - // timeline[1], - // _header, - // ); - // expect( - // timeline[2], - // 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', - // ); - // expect( - // timeline[3].startsWith('Caller: at main. file:///'), - // isTrue, - // ); - // expect( - // timeline[4].startsWith( - // 'Screenshot: file:///', - // ), - // isTrue, - // ); - // expect( - // timeline[5].startsWith( - // 'Timestamp:', - // ), - // isTrue, - // ); - // expect( - // timeline[6], - // _separator, - // ); - // final htmlLine = timeline - // .firstWhere((line) => line.startsWith('View time line here:')); - // expect( - // htmlLine.endsWith( - // 'timeline_onerror_timeline_with_error.html', - // ), - // isTrue, - // ); - // } - // }); + test('OnError timeline - with error, prints timeline', () async { + final tempDir = Directory.systemTemp.createTempSync(); + final tempTestFile = File('${tempDir.path}/temp_test.dart'); + await tempTestFile.writeAsString( + _testAsString( + title: 'Drag - OnError timeline with error', + timelineMode: TimelineMode.record, + shouldFail: true, + ), + ); + final testProcess = + await TestProcess.start('flutter', ['test', tempTestFile.path]); + final stdoutBuffer = StringBuffer(); + bool write = false; + + await for (final line in testProcess.stdoutStream()) { + if (line.isEmpty) continue; + + if (!write) { + if (line == 'Timeline') { + write = true; + } + } + + if (write) { + stdoutBuffer.writeln(line); + } + } + // Error happens + await testProcess.shouldExit(1); + if (tempDir.existsSync()) { + tempDir.deleteSync(recursive: true); + } + final stdout = stdoutBuffer.toString(); + _testTimeLineContent( + output: stdout, + drags: 10, + totalExpectedOffset: const Offset(0, -1000), + findsWidget: false, + ); + + final htmlLine = stdout + .split('\n') + .firstWhere((line) => line.startsWith('View time line here:')); + + expect( + htmlLine.endsWith( + 'timeline_drag_-_onerror_timeline_with_error.html', + ), + isTrue, + ); + }); // test('Live timeline - without error, prints HTML', () async { // final tempDir = Directory.systemTemp.createTempSync(); // final tempTestFile = File('${tempDir.path}/temp_test.dart'); @@ -357,27 +338,25 @@ void main() { void _testTimeLineContent({ required String output, - required int dragEvents, bool findsWidget = true, - required WidgetSelector targetSelector, required Offset totalExpectedOffset, - int? drags, + required int drags, }) { final eventLines = output.split('\n').where((line) => line.startsWith('Event:')); final startEvent = _replaceOffsetWithDxDy(eventLines.first); final endEvent = - 'Event: Target ${targetSelector.toStringBreadcrumb()} ${findsWidget ? 'found' : ''} after $drags drags. Total dragged offset: $totalExpectedOffset'; + 'Event: Target ${_secondItemSelector.toStringBreadcrumb()} ${findsWidget ? 'found' : 'not found'} after $drags drags. Total dragged offset: $totalExpectedOffset'; for (int i = 0; i < 2; i++) { expect( RegExp(_header).allMatches(output).length, - dragEvents, + 2, ); if (i == 0) { expect( startEvent, - 'Event: Starting to drag from Offset(dx,dy) towards ${targetSelector.toStringBreadcrumb()}.', + 'Event: Starting to drag from Offset(dx,dy) towards ${_secondItemSelector.toStringBreadcrumb()}.', ); } else { expect( @@ -386,18 +365,16 @@ void _testTimeLineContent({ ); } expect( - RegExp('Caller: at main... file:///') - .allMatches(output) - .length, - dragEvents, + RegExp('Caller: at main').allMatches(output).length, + 2, ); expect( RegExp('Screenshot: file:').allMatches(output).length, - dragEvents, + 2, ); expect( RegExp('Timestamp: ').allMatches(output).length, - dragEvents, + 2, ); } } diff --git a/test/timeline/tap/timeline_tap_test.dart b/test/timeline/tap/timeline_tap_test.dart index f3b51e1b..ee5e154b 100644 --- a/test/timeline/tap/timeline_tap_test.dart +++ b/test/timeline/tap/timeline_tap_test.dart @@ -197,7 +197,10 @@ void main() { // Error happens await testProcess.shouldExit(1); - tempDir.deleteSync(recursive: true); + + if (tempDir.existsSync()) { + tempDir.deleteSync(recursive: true); + } final stdout = stdoutBuffer.toString(); final timeline = stdout.split('\n'); @@ -272,7 +275,10 @@ void main() { // Error does not happen await testProcess.shouldExit(0); - tempDir.deleteSync(recursive: true); + + if (tempDir.existsSync()) { + tempDir.deleteSync(recursive: true); + } final stdout = stdoutBuffer.toString(); final timeline = stdout.split('\n'); @@ -344,7 +350,10 @@ void main() { // Error does not happen await testProcess.shouldExit(1); - tempDir.deleteSync(recursive: true); + + if (tempDir.existsSync()) { + tempDir.deleteSync(recursive: true); + } final stdout = stdoutBuffer.toString(); final timeline = stdout.split('\n'); From 87c9020c25dc1100fee354e337407f28ba953efe Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 21 Jun 2024 02:28:14 +0200 Subject: [PATCH 080/119] Improve timeline message --- lib/src/act/act.dart | 5 ++++- test/timeline/drag/timeline_drag_test.dart | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/src/act/act.dart b/lib/src/act/act.dart index e4852176..22a920d9 100644 --- a/lib/src/act/act.dart +++ b/lib/src/act/act.dart @@ -201,8 +201,11 @@ class Act { return; } + final direction = moveStep.dy < 0 ? 'downwards' : 'upwards'; + await addDragEvent( - name: 'Starting to drag from $dragPosition towards $targetName.', + name: + 'Scrolling $direction from $dragPosition in order to find $targetName.', ); int dragCount = 0; diff --git a/test/timeline/drag/timeline_drag_test.dart b/test/timeline/drag/timeline_drag_test.dart index 1f28a445..46db4caa 100644 --- a/test/timeline/drag/timeline_drag_test.dart +++ b/test/timeline/drag/timeline_drag_test.dart @@ -342,6 +342,7 @@ void _testTimeLineContent({ required Offset totalExpectedOffset, required int drags, }) { + final isGoingDown = totalExpectedOffset.dy < 0; final eventLines = output.split('\n').where((line) => line.startsWith('Event:')); final startEvent = _replaceOffsetWithDxDy(eventLines.first); @@ -356,7 +357,7 @@ void _testTimeLineContent({ if (i == 0) { expect( startEvent, - 'Event: Starting to drag from Offset(dx,dy) towards ${_secondItemSelector.toStringBreadcrumb()}.', + 'Event: Scrolling ${isGoingDown ? 'downwards' : 'upwards'} from Offset(dx,dy) in order to find ${_secondItemSelector.toStringBreadcrumb()}.', ); } else { expect( From 3168901e0d2568dc2641eb23140c92d5c862b262 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 21 Jun 2024 13:19:56 +0200 Subject: [PATCH 081/119] Add test "act.drag: OnError timeline - with error, prints timeline'" --- lib/src/timeline/timeline.dart | 37 ++++++++-- test/timeline/drag/timeline_drag_test.dart | 67 +++++++++++++++++-- test/timeline/tap/timeline_tap_test.dart | 10 +-- ...output.dart => timeline_test_helpers.dart} | 0 4 files changed, 98 insertions(+), 16 deletions(-) rename test/util/{capture_console_output.dart => timeline_test_helpers.dart} (100%) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index b1710631..99d244bc 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -134,16 +134,41 @@ class Timeline { /// Prints the timeline as an HTML file. void _printHTML() { final spotTempDir = Directory.systemTemp.createTempSync(); - final String name = (Invoker.current?.liveTest.test.name ?? '') - .trim() - .toLowerCase() - .replaceAll(' ', '_'); - if (name.isEmpty) return; + String name = Invoker.current?.liveTest.test.name ?? ''; + if (name.isEmpty) { + name = 'Unnamed test'; + } if (!spotTempDir.existsSync()) { spotTempDir.createSync(); } - final htmlFile = File(path.join(spotTempDir.path, 'timeline_$name.html')); + final String nameForHtml = () { + String name = Invoker.current?.liveTest.test.name ?? ''; + + if (name.isEmpty) { + name = 'Unnamed test'; + } + + // Replace spaces and underscores with hyphens + name = name.replaceAll(RegExp('[ _]'), '-'); + + // Remove problematic characters + name = name.replaceAll(RegExp('[^a-zA-Z0-9-]'), ''); + + // Collapse multiple hyphens into a single hyphen + name = name.replaceAll(RegExp('-+'), '-'); + + // Convert to lowercase + name = name.toLowerCase(); + + // Remove leading or trailing hyphens + name = name.replaceAll(RegExp(r'^-+|-+$'), ''); + + // Append .html extension + return 'timeline-$name.html'; + }(); + + final htmlFile = File(path.join(spotTempDir.path, nameForHtml)); final content = _timelineAsHTML(); htmlFile.writeAsStringSync(content); //ignore: avoid_print diff --git a/test/timeline/drag/timeline_drag_test.dart b/test/timeline/drag/timeline_drag_test.dart index 46db4caa..928b8a1b 100644 --- a/test/timeline/drag/timeline_drag_test.dart +++ b/test/timeline/drag/timeline_drag_test.dart @@ -5,7 +5,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import 'package:spot/src/timeline/timeline.dart'; import 'package:test_process/test_process.dart'; -import '../../util/capture_console_output.dart'; +import '../../util/timeline_test_helpers.dart'; import 'drag_until_visible_test_widget.dart'; final _firstItemSelector = spotText('Item at index: 3', exact: true); @@ -131,8 +131,8 @@ void main() { expect(output, contains('鈴革笌 - Timeline recording is off')); }); - group('Print on teardown', () { - testWidgets('OnError timeline - without error', (tester) async { + group('act.drag: Print on teardown', () { + testWidgets('act.drag: OnError timeline - without error', (tester) async { final output = await captureConsoleOutput(() async { recordOnErrorTimeline(); await tester.pumpWidget(const DragUntilVisibleTestWidget()); @@ -153,7 +153,8 @@ void main() { expect(splitted.first, '馃敶 - Now recording error output timeline'); expect(splitted.second, '馃敶 - Already recording error output timeline'); }); - test('OnError timeline - with error, prints timeline', () async { + test('act.drag: OnError timeline - with error, prints timeline', + () async { final tempDir = Directory.systemTemp.createTempSync(); final tempTestFile = File('${tempDir.path}/temp_test.dart'); await tempTestFile.writeAsString( @@ -200,7 +201,63 @@ void main() { expect( htmlLine.endsWith( - 'timeline_drag_-_onerror_timeline_with_error.html', + 'timeline-drag-onerror-timeline-with-error.html', + ), + isTrue, + ); + }); + test('act.drag: Live timeline - without error, prints HTML', () async { + final tempDir = Directory.systemTemp.createTempSync(); + final tempTestFile = File('${tempDir.path}/temp_test.dart'); + await tempTestFile.writeAsString( + _testAsString( + title: 'act.drag: Live timeline - without error, prints HTML', + timelineMode: TimelineMode.live, + ), + ); + final testProcess = + await TestProcess.start('flutter', ['test', tempTestFile.path]); + final stdoutBuffer = StringBuffer(); + bool write = false; + + await for (final line in testProcess.stdoutStream()) { + if (line.isEmpty) continue; + + if (!write) { + if (line == 'Timeline' || line == _header) { + write = true; + } + } + + if (write) { + stdoutBuffer.writeln(line); + } + } + + await testProcess.shouldExit(0); + + if (tempDir.existsSync()) { + tempDir.deleteSync(recursive: true); + } + + final stdout = stdoutBuffer.toString(); + + // Does not start with 'Timeline', this only happens on error + expect(stdout.startsWith('Timeline'), isFalse); + + _testTimeLineContent( + output: stdout, + drags: 23, + totalExpectedOffset: const Offset(0, -2300), + ); + + final htmlLine = stdout + .split('\n') + .firstWhere((line) => line.startsWith('View time line here:')); + + expect( + htmlLine.endsWith( + 'timeline-actdrag-live-timeline-without-error-prints-html.html', ), isTrue, ); diff --git a/test/timeline/tap/timeline_tap_test.dart b/test/timeline/tap/timeline_tap_test.dart index ee5e154b..8aaea61c 100644 --- a/test/timeline/tap/timeline_tap_test.dart +++ b/test/timeline/tap/timeline_tap_test.dart @@ -6,7 +6,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import 'package:spot/src/timeline/timeline.dart'; import 'package:test_process/test_process.dart'; -import '../../util/capture_console_output.dart'; +import '../../util/timeline_test_helpers.dart'; import 'timeline_tap_test_widget.dart'; final _addButtonSelector = spotIcon(Icons.add); @@ -173,7 +173,7 @@ void main() { final tempTestFile = File('${tempDir.path}/temp_test.dart'); await tempTestFile.writeAsString( _testAsString( - title: 'OnError timeline with error', + title: 'OnError timeline - with error, prints timeline', timelineMode: TimelineMode.record, shouldFail: true, ), @@ -238,7 +238,7 @@ void main() { .firstWhere((line) => line.startsWith('View time line here:')); expect( htmlLine.endsWith( - 'timeline_onerror_timeline_with_error.html', + 'timeline-onerror-timeline-with-error-prints-timeline.html', ), isTrue, ); @@ -312,7 +312,7 @@ void main() { .firstWhere((line) => line.startsWith('View time line here:')); expect( htmlLine.endsWith( - 'timeline_live_timeline_without_error_prints_html.html', + 'timeline-live-timeline-without-error-prints-html.html', ), isTrue, ); @@ -387,7 +387,7 @@ void main() { .firstWhere((line) => line.startsWith('View time line here:')); expect( htmlLine.endsWith( - 'live_timeline_-_with_error,_no_duplicates,_prints_html.html', + 'live-timeline-with-error-no-duplicates-prints-html.html', ), isTrue, ); diff --git a/test/util/capture_console_output.dart b/test/util/timeline_test_helpers.dart similarity index 100% rename from test/util/capture_console_output.dart rename to test/util/timeline_test_helpers.dart From f8f1c0d47a5efaa934ccd19477ba2f2b5ba0d1fd Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 21 Jun 2024 15:19:31 +0200 Subject: [PATCH 082/119] Refactor tests --- test/timeline/drag/timeline_drag_test.dart | 271 ++++++++------------- test/timeline/tap/timeline_tap_test.dart | 23 +- test/util/timeline_test_helpers.dart | 13 + 3 files changed, 114 insertions(+), 193 deletions(-) diff --git a/test/timeline/drag/timeline_drag_test.dart b/test/timeline/drag/timeline_drag_test.dart index 928b8a1b..9b26f3f6 100644 --- a/test/timeline/drag/timeline_drag_test.dart +++ b/test/timeline/drag/timeline_drag_test.dart @@ -5,31 +5,25 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import 'package:spot/src/timeline/timeline.dart'; import 'package:test_process/test_process.dart'; -import '../../util/timeline_test_helpers.dart'; +import '../../util/timeline_test_helpers.dart' as helpers; import 'drag_until_visible_test_widget.dart'; final _firstItemSelector = spotText('Item at index: 3', exact: true); final _secondItemSelector = spotText('Item at index: 27', exact: true); +const _failingDragAmount = 10; +const _failingOffset = Offset(0, -1000); +const _passingDragAmount = 23; +const _passingOffset = Offset(0, -2300); + const _header = '==================== Timeline Event ===================='; // const _separator = '========================================================'; String _testAsString({ required String title, required TimelineMode timelineMode, - bool shouldFail = false, + required int drags, }) { - final String methodForMode = () { - switch (timelineMode) { - case TimelineMode.live: - return 'recordLiveTimeline()'; - case TimelineMode.record: - return 'recordOnErrorTimeline()'; - case TimelineMode.off: - return 'stopRecordingTimeline()'; - } - }(); - final widgetPart = File('test/timeline/drag/drag_until_visible_test_widget.dart') .readAsStringSync(); @@ -40,7 +34,7 @@ import 'package:spot/src/timeline/timeline.dart';\n $widgetPart\n void main() async { testWidgets("$title", (WidgetTester tester) async { - $methodForMode; + ${helpers.timelineInitiatorForModeAsString(timelineMode)}; await tester.pumpWidget(const DragUntilVisibleTestWidget()); final firstItem = spotText('Item at index: 3', exact: true)..existsOnce(); final secondItem = spotText('Item at index: 27', exact: true) @@ -48,7 +42,7 @@ void main() async { await act.dragUntilVisible( dragStart: firstItem, dragTarget: secondItem, - maxIteration: ${shouldFail ? '10' : '30'}, + maxIteration: $drags, moveStep: const Offset(0, -100), ); secondItem.existsOnce(); @@ -57,111 +51,92 @@ void main() async { '''; } +Future _testBody(WidgetTester tester) async { + await tester.pumpWidget(const DragUntilVisibleTestWidget()); + _firstItemSelector.existsOnce(); + _secondItemSelector.doesNotExist(); + await act.dragUntilVisible( + dragStart: _firstItemSelector, + dragTarget: _secondItemSelector, + moveStep: const Offset(0, -100), + maxIteration: _passingDragAmount, + ); + _secondItemSelector.existsOnce(); +} + void main() { group('Drag Timeline Test', () { - testWidgets('Drag Until Visible - Live timeline', (tester) async { - final output = await captureConsoleOutput(() async { - recordLiveTimeline(); - await tester.pumpWidget(const DragUntilVisibleTestWidget()); - _firstItemSelector.existsOnce(); - _secondItemSelector.doesNotExist(); - await act.dragUntilVisible( - dragStart: _firstItemSelector, - dragTarget: _secondItemSelector, - maxIteration: 30, - moveStep: const Offset(0, -100), + group('Without error', () { + testWidgets('Drag Until Visible - Live timeline', (tester) async { + final output = await helpers.captureConsoleOutput(() async { + recordLiveTimeline(); + await _testBody(tester); + // Notify that the timeline of this type is already recording. + recordLiveTimeline(); + }); + expect(output, contains('馃敶 - Now recording live timeline')); + _testTimeLineContent( + output: output, + totalExpectedOffset: _passingOffset, + drags: _passingDragAmount, ); - _secondItemSelector.existsOnce(); - // Notify that the timeline of this type is already recording. - recordLiveTimeline(); + expect(output, contains('馃敶 - Already recording live timeline')); }); - expect(output, contains('馃敶 - Now recording live timeline')); - _testTimeLineContent( - output: output, - totalExpectedOffset: const Offset(0, -2300), - drags: 23, - ); - expect(output, contains('馃敶 - Already recording live timeline')); - }); - testWidgets('Start with Timeline Mode off', (tester) async { - final output = await captureConsoleOutput(() async { - stopRecordingTimeline(); - await tester.pumpWidget(const DragUntilVisibleTestWidget()); - _firstItemSelector.existsOnce(); - _secondItemSelector.doesNotExist(); - await act.dragUntilVisible( - dragStart: _firstItemSelector, - dragTarget: _secondItemSelector, - maxIteration: 30, - moveStep: const Offset(0, -100), - ); - _secondItemSelector.existsOnce(); + testWidgets('Start with Timeline Mode off', (tester) async { + final output = await helpers.captureConsoleOutput(() async { + stopRecordingTimeline(); + await _testBody(tester); + }); + final splitted = output.split('\n') + ..removeWhere((line) => line.isEmpty); + const expectedOutput = '鈴革笌 - Timeline recording is off'; + expect(splitted.length, 1); + expect(splitted.first, expectedOutput); }); - final splitted = output.split('\n')..removeWhere((line) => line.isEmpty); - const expectedOutput = '鈴革笌 - Timeline recording is off'; - expect(splitted.length, 1); - expect(splitted.first, expectedOutput); - }); - testWidgets('Turn timeline mode off during test', (tester) async { - final output = await captureConsoleOutput(() async { - recordLiveTimeline(); - await tester.pumpWidget(const DragUntilVisibleTestWidget()); - _firstItemSelector.existsOnce(); - _secondItemSelector.doesNotExist(); - await act.dragUntilVisible( - dragStart: _firstItemSelector, - dragTarget: _secondItemSelector, - maxIteration: 30, - moveStep: const Offset(0, -100), + testWidgets('Turn timeline mode off during test', (tester) async { + final output = await helpers.captureConsoleOutput(() async { + recordLiveTimeline(); + await _testBody(tester); + // Notify that the recording is off + stopRecordingTimeline(); + stopRecordingTimeline(); + }); + expect(output, contains('馃敶 - Now recording live timeline')); + + _testTimeLineContent( + output: output, + totalExpectedOffset: _passingOffset, + drags: _passingDragAmount, ); - _secondItemSelector.existsOnce(); - // Notify that the recording is off - stopRecordingTimeline(); - stopRecordingTimeline(); + expect(output, contains('鈴革笌 - Timeline recording stopped')); + expect(output, contains('鈴革笌 - Timeline recording is off')); }); - expect(output, contains('馃敶 - Now recording live timeline')); - _testTimeLineContent( - output: output, - totalExpectedOffset: const Offset(0, -2300), - drags: 23, - ); - - expect(output, contains('鈴革笌 - Timeline recording stopped')); - expect(output, contains('鈴革笌 - Timeline recording is off')); - }); - - group('act.drag: Print on teardown', () { testWidgets('act.drag: OnError timeline - without error', (tester) async { - final output = await captureConsoleOutput(() async { + final output = await helpers.captureConsoleOutput(() async { recordOnErrorTimeline(); - await tester.pumpWidget(const DragUntilVisibleTestWidget()); - _firstItemSelector.existsOnce(); - _secondItemSelector.doesNotExist(); - await act.dragUntilVisible( - dragStart: _firstItemSelector, - dragTarget: _secondItemSelector, - maxIteration: 30, - moveStep: const Offset(0, -100), - ); - _secondItemSelector.existsOnce(); + await _testBody(tester); recordOnErrorTimeline(); }); - final splitted = output.split('\n') - ..removeWhere((line) => line.isEmpty); - expect(splitted.length, 2); - expect(splitted.first, '馃敶 - Now recording error output timeline'); - expect(splitted.second, '馃敶 - Already recording error output timeline'); + final lines = output.split('\n')..removeWhere((line) => line.isEmpty); + expect(lines.first, '馃敶 - Now recording error output timeline'); + expect(lines.second, '馃敶 - Already recording error output timeline'); + + // Neither timeline output nor HTML link when onError timeline is + // recorded and no error occurs. + expect(lines.length, 2); }); - test('act.drag: OnError timeline - with error, prints timeline', - () async { + }); + group('Teardown test', () { + test('OnError timeline - with error, prints timeline and html', () async { final tempDir = Directory.systemTemp.createTempSync(); final tempTestFile = File('${tempDir.path}/temp_test.dart'); + await tempTestFile.writeAsString( _testAsString( - title: 'Drag - OnError timeline with error', + title: 'OnError timeline - with error, prints timeline', timelineMode: TimelineMode.record, - shouldFail: true, + drags: _failingDragAmount, ), ); final testProcess = @@ -184,15 +159,17 @@ void main() { } // Error happens await testProcess.shouldExit(1); + if (tempDir.existsSync()) { tempDir.deleteSync(recursive: true); } + final stdout = stdoutBuffer.toString(); _testTimeLineContent( output: stdout, - drags: 10, - totalExpectedOffset: const Offset(0, -1000), - findsWidget: false, + drags: _failingDragAmount, + totalExpectedOffset: _failingOffset, + runInTestProcess: true, ); final htmlLine = stdout @@ -201,18 +178,19 @@ void main() { expect( htmlLine.endsWith( - 'timeline-drag-onerror-timeline-with-error.html', + 'timeline-onerror-timeline-with-error-prints-timeline.html', ), isTrue, ); }); - test('act.drag: Live timeline - without error, prints HTML', () async { + test('Live timeline - without error, prints HTML', () async { final tempDir = Directory.systemTemp.createTempSync(); final tempTestFile = File('${tempDir.path}/temp_test.dart'); await tempTestFile.writeAsString( _testAsString( - title: 'act.drag: Live timeline - without error, prints HTML', + title: 'Live timeline - without error, prints HTML', timelineMode: TimelineMode.live, + drags: _passingDragAmount, ), ); final testProcess = @@ -247,8 +225,9 @@ void main() { _testTimeLineContent( output: stdout, - drags: 23, - totalExpectedOffset: const Offset(0, -2300), + drags: _passingDragAmount, + totalExpectedOffset: _passingOffset, + runInTestProcess: true, ); final htmlLine = stdout @@ -257,74 +236,11 @@ void main() { expect( htmlLine.endsWith( - 'timeline-actdrag-live-timeline-without-error-prints-html.html', + 'timeline-live-timeline-without-error-prints-html.html', ), isTrue, ); }); - // test('Live timeline - without error, prints HTML', () async { - // final tempDir = Directory.systemTemp.createTempSync(); - // final tempTestFile = File('${tempDir.path}/temp_test.dart'); - // await tempTestFile.writeAsString( - // _testAsString( - // title: 'Live timeline without error prints html', - // timelineMode: TimelineMode.live, - // ), - // ); - // final testProcess = - // await TestProcess.start('flutter', ['test', tempTestFile.path]); - // final stdoutBuffer = StringBuffer(); - // bool write = false; - // await for (final line in testProcess.stdoutStream()) { - // if (line.isEmpty) continue; - // if (!write) { - // if (line == _header) { - // write = true; - // } - // if (write) { - // stdoutBuffer.writeln(line); - // } // Error does not happen - // } - // await testProcess.shouldExit(0); - // tempDir.deleteSync(recursive: true); - // final stdout = stdoutBuffer.toString(); - // final timeline = stdout.split('\n'); - // // Does not start with 'Timeline', this only happens on error - // expect(timeline.first, _header); - // expect( - // timeline.second, - // 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', - // ); - // expect( - // timeline[2].startsWith('Caller: at main. file:///'), - // isTrue, - // ); - // expect( - // timeline[3].startsWith( - // 'Screenshot: file:///', - // ), - // isTrue, - // ); - // expect( - // timeline[4].startsWith( - // 'Timestamp:', - // ), - // isTrue, - // ); - // expect( - // timeline[5], - // _separator, - // ); - // final htmlLine = timeline - // .firstWhere((line) => line.startsWith('View time line here:')); - // expect( - // htmlLine.endsWith( - // 'timeline_live_timeline_without_error_prints_html.html', - // ), - // isTrue, - // ); - // } - // }); // test('Live timeline - with error, no duplicates, prints HTML', () async { // final tempDir = Directory.systemTemp.createTempSync(); // final tempTestFile = File('${tempDir.path}/temp_test.dart'); @@ -395,11 +311,12 @@ void main() { void _testTimeLineContent({ required String output, - bool findsWidget = true, required Offset totalExpectedOffset, required int drags, + bool runInTestProcess = false, }) { final isGoingDown = totalExpectedOffset.dy < 0; + final findsWidget = drags == _passingDragAmount; final eventLines = output.split('\n').where((line) => line.startsWith('Event:')); final startEvent = _replaceOffsetWithDxDy(eventLines.first); @@ -423,7 +340,9 @@ void _testTimeLineContent({ ); } expect( - RegExp('Caller: at main').allMatches(output).length, + RegExp('Caller: at ${runInTestProcess ? 'main' : '_testBody'}') + .allMatches(output) + .length, 2, ); expect( diff --git a/test/timeline/tap/timeline_tap_test.dart b/test/timeline/tap/timeline_tap_test.dart index 8aaea61c..62c786f8 100644 --- a/test/timeline/tap/timeline_tap_test.dart +++ b/test/timeline/tap/timeline_tap_test.dart @@ -6,7 +6,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import 'package:spot/src/timeline/timeline.dart'; import 'package:test_process/test_process.dart'; -import '../../util/timeline_test_helpers.dart'; +import '../../util/timeline_test_helpers.dart' as helpers; import 'timeline_tap_test_widget.dart'; final _addButtonSelector = spotIcon(Icons.add); @@ -21,17 +21,6 @@ String _testAsString({ required TimelineMode timelineMode, bool shouldFail = false, }) { - final String methodForMode = () { - switch (timelineMode) { - case TimelineMode.live: - return 'recordLiveTimeline()'; - case TimelineMode.record: - return 'recordOnErrorTimeline()'; - case TimelineMode.off: - return 'stopRecordingTimeline()'; - } - }(); - final widgetPart = File('test/timeline/tap/timeline_tap_test_widget.dart') .readAsStringSync(); return ''' @@ -43,7 +32,7 @@ void main() async { final addButtonSelector = spotIcon(Icons.add); final subtractButtonSelector = spotIcon(Icons.remove); testWidgets("$title", (WidgetTester tester) async { - $methodForMode; + ${helpers.timelineInitiatorForModeAsString(timelineMode)}; await tester.pumpWidget(const TimelineTestWidget()); addButtonSelector.existsOnce(); spotText('Counter: 3').existsOnce(); @@ -59,7 +48,7 @@ void main() async { void main() { testWidgets('Live timeline', (tester) async { - final output = await captureConsoleOutput(() async { + final output = await helpers.captureConsoleOutput(() async { recordLiveTimeline(); await tester.pumpWidget(const TimelineTestWidget()); _addButtonSelector.existsOnce(); @@ -84,7 +73,7 @@ void main() { _testTimeLineContent(output: output, eventCount: 2); }); testWidgets('Start with Timeline Mode off', (tester) async { - final output = await captureConsoleOutput(() async { + final output = await helpers.captureConsoleOutput(() async { stopRecordingTimeline(); await tester.pumpWidget(const TimelineTestWidget()); _addButtonSelector.existsOnce(); @@ -107,7 +96,7 @@ void main() { _testTimeLineContent(output: output, eventCount: 0); }); testWidgets('Turn timeline mode off during test', (tester) async { - final output = await captureConsoleOutput(() async { + final output = await helpers.captureConsoleOutput(() async { recordLiveTimeline(); await tester.pumpWidget( const TimelineTestWidget(), @@ -143,7 +132,7 @@ void main() { group('Print on teardown', () { testWidgets('OnError timeline - without error', (tester) async { - final output = await captureConsoleOutput(() async { + final output = await helpers.captureConsoleOutput(() async { recordOnErrorTimeline(); await tester.pumpWidget(const TimelineTestWidget()); _addButtonSelector.existsOnce(); diff --git a/test/util/timeline_test_helpers.dart b/test/util/timeline_test_helpers.dart index c34ee4d0..f5d83693 100644 --- a/test/util/timeline_test_helpers.dart +++ b/test/util/timeline_test_helpers.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:spot/src/timeline/timeline.dart'; + Future captureConsoleOutput( Future Function() testFunction, ) async { @@ -16,3 +18,14 @@ Future captureConsoleOutput( return buffer.toString(); } + +String timelineInitiatorForModeAsString(TimelineMode timelineMode) { + switch (timelineMode) { + case TimelineMode.live: + return 'recordLiveTimeline()'; + case TimelineMode.record: + return 'recordOnErrorTimeline()'; + case TimelineMode.off: + return 'stopRecordingTimeline()'; + } +} From 95cbbd889f3779bcf8314a1cc6f1cd5da1ac28bc Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Mon, 24 Jun 2024 14:37:43 +0200 Subject: [PATCH 083/119] Make timeline a global getter and the TimelineMode adjustable with --dart-define=SPOT_TIMELINE_MODE=live|record|off --- lib/src/act/act.dart | 2 -- lib/src/timeline/timeline.dart | 44 ++++++++++++++++++++++++++++------ 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/lib/src/act/act.dart b/lib/src/act/act.dart index 22a920d9..f2478491 100644 --- a/lib/src/act/act.dart +++ b/lib/src/act/act.dart @@ -89,7 +89,6 @@ class Act { final centerPosition = renderBox.localToGlobal(renderBox.size.center(Offset.zero)); - final timeline = currentTimeline(); if (timeline.mode != TimelineMode.off) { final screenshot = await takeScreenshotWithCrosshair( centerPosition: centerPosition, @@ -177,7 +176,6 @@ class Act { Future addDragEvent({ required String name, }) async { - final timeline = currentTimeline(); if (timeline.mode != TimelineMode.off) { final screenshot = await takeScreenshotWithCrosshair( centerPosition: dragPosition, diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index 99d244bc..73d3aad9 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -11,11 +11,8 @@ import 'package:test_api/src/backend/invoker.dart'; //ignore: implementation_imports import 'package:test_api/src/backend/live_test.dart'; -final Map _timelines = {}; - /// Records the timeline and prints events as they happen. void recordLiveTimeline() { - final timeline = currentTimeline(); final isRecordingLive = timeline.mode == TimelineMode.live; final message = isRecordingLive ? 'Already recording' : 'Now recording'; // ignore: avoid_print @@ -25,7 +22,6 @@ void recordLiveTimeline() { /// Records the timeline but only prints it in case of an error. void recordOnErrorTimeline() { - final timeline = currentTimeline(); final isRecordingError = timeline.mode == TimelineMode.record; final message = isRecordingError ? 'Already' : 'Now'; // ignore: avoid_print @@ -35,7 +31,6 @@ void recordOnErrorTimeline() { /// Stops the timeline from recording. void stopRecordingTimeline() { - final timeline = currentTimeline(); final isRecording = timeline.mode != TimelineMode.off; final message = isRecording ? 'stopped' : 'is off'; // ignore: avoid_print @@ -43,12 +38,44 @@ void stopRecordingTimeline() { timeline.mode = TimelineMode.off; } +/// Use to set the timeline mode for all tests in a test file. +/// +/// ```dart +/// void main() { +/// globalTimelineMode = TimelineMode.live; +/// +/// testWidgets('Test 1', (tester) async { +/// // ... +/// }); +/// +/// testWidgets('Test 2', (tester) async { +/// // ... +/// }); +/// } +/// ``` +TimelineMode globalTimelineMode = + getTimelineModeFromEnv() ?? TimelineMode.record; + +/// Use --dart-define=SPOT_TIMELINE_MODE=live|record|off to set the [TimlineMode] +/// for all tests +TimelineMode? getTimelineModeFromEnv() { + final mode = const String.fromEnvironment('SPOT_TIMELINE_MODE').toLowerCase(); + return switch (mode) { + 'live' => TimelineMode.live, + 'record' => TimelineMode.record, + 'off' => TimelineMode.off, + _ => null, + }; +} + +final Map _timelines = {}; + /// Returns the current timeline for the test or creates a new one if /// it doesn't exist. -Timeline currentTimeline() { +Timeline get timeline { final test = Invoker.current?.liveTest; if (test == null) { - throw StateError('currentTimeline() must be called within a test'); + throw StateError('timeline must be called within a test'); } final timeline = _timelines[test]; if (timeline != null) { @@ -58,6 +85,7 @@ Timeline currentTimeline() { // create new timeline final newTimeline = Timeline(); + newTimeline.mode = globalTimelineMode; Invoker.current!.addTearDown(() { if (!test.state.result.isPassing && @@ -69,7 +97,9 @@ Timeline currentTimeline() { // the timeline is already being printed live newTimeline._printHTML(); } + _timelines.remove(test); }); + _timelines[test] = newTimeline; return newTimeline; } From 3c00fda3ce20e318ba92060ece0cf29d54854d2a Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Mon, 24 Jun 2024 14:52:26 +0200 Subject: [PATCH 084/119] Make timeline.addEvent public --- lib/src/act/act.dart | 4 +-- lib/src/timeline/timeline.dart | 59 +++++++++++++++++++++++----------- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/lib/src/act/act.dart b/lib/src/act/act.dart index f2478491..40bd3189 100644 --- a/lib/src/act/act.dart +++ b/lib/src/act/act.dart @@ -96,7 +96,7 @@ class Act { timeline.addScreenshot( screenshot, name: 'Tap ${selector.toStringBreadcrumb()}', - eventType: TimelineEventType.tap, + eventType: const TimelineEventType(label: 'tap'), ); } @@ -183,7 +183,7 @@ class Act { timeline.addScreenshot( screenshot, name: name, - eventType: TimelineEventType.drag, + eventType: const TimelineEventType(label: 'drag'), ); } } diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index 73d3aad9..033ca90b 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -6,6 +6,7 @@ import 'package:spot/src/screenshot/screenshot.dart'; import 'package:spot/src/spot/tree_snapshot.dart'; import 'package:spot/src/timeline/script.js.dart'; import 'package:spot/src/timeline/styles.css.dart'; +import 'package:stack_trace/stack_trace.dart'; //ignore: implementation_imports import 'package:test_api/src/backend/invoker.dart'; //ignore: implementation_imports @@ -131,7 +132,7 @@ class Timeline { String? name, TimelineEventType? eventType, }) { - _addEvent( + addRawEvent( TimelineEvent.now( name: name, screenshot: screenshot, @@ -142,7 +143,29 @@ class Timeline { } /// Adds an event to the timeline. - void _addEvent(TimelineEvent event) { + void addEvent({ + String? name, + Frame? initiator, + Screenshot? screenshot, + String? eventType, + String? description, + }) { + addRawEvent( + TimelineEvent( + name: name, + screenshot: screenshot, + initiator: + initiator ?? screenshot?.initiator ?? Trace.current().frames[1], + timestamp: DateTime.now(), + treeSnapshot: currentWidgetTreeSnapshot(), + eventType: + eventType != null ? TimelineEventType(label: eventType) : null, + ), + ); + } + + /// Adds an event to the timeline. + void addRawEvent(TimelineEvent event) { if (mode == TimelineMode.off) { return; } @@ -334,33 +357,28 @@ class Timeline { } /// The type of event that occurred during a test. -enum TimelineEventType { - /// A tap event. - tap( - 'Tap Event (crosshair indicator)', - ), - - /// A drag event. - drag( - 'Drag Event (crosshair indicator)', - ); - - const TimelineEventType(this.label); - +class TimelineEventType { /// The name of the event. final String label; + + // TODO add styling information like color? + + const TimelineEventType({ + required this.label, + }); } /// An event that occurred during a test. class TimelineEvent { /// Creates a new timeline event. const TimelineEvent({ - this.screenshot, - this.name, - this.initiator, - this.eventType, required this.timestamp, required this.treeSnapshot, + this.name, + this.eventType, + this.description, + this.initiator, + this.screenshot, }); /// Creates a new timeline event with the current time and widget tree snapshot. @@ -397,6 +415,9 @@ class TimelineEvent { /// The frame that initiated the event. final Frame? initiator; + + /// Custom plain-text information about the event. + final String? description; } /// The mode of the timeline. From 4366e12d617c2c2f72c7a0afcafc398b35bcd813 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Tue, 25 Jun 2024 11:46:55 +0200 Subject: [PATCH 085/119] Make local timeline activation work when global is off --- lib/src/timeline/timeline.dart | 8 +- test/timeline/tap/timeline_tap_test.dart | 165 ++++++++++++----------- 2 files changed, 93 insertions(+), 80 deletions(-) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index 033ca90b..939833dd 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -54,8 +54,12 @@ void stopRecordingTimeline() { /// }); /// } /// ``` -TimelineMode globalTimelineMode = - getTimelineModeFromEnv() ?? TimelineMode.record; +TimelineMode globalTimelineMode = defaultTimelineMode(); + +/// Returns the default timeline mode. +TimelineMode defaultTimelineMode() { + return getTimelineModeFromEnv() ?? TimelineMode.record; +} /// Use --dart-define=SPOT_TIMELINE_MODE=live|record|off to set the [TimlineMode] /// for all tests diff --git a/test/timeline/tap/timeline_tap_test.dart b/test/timeline/tap/timeline_tap_test.dart index 62c786f8..10ddd62a 100644 --- a/test/timeline/tap/timeline_tap_test.dart +++ b/test/timeline/tap/timeline_tap_test.dart @@ -47,88 +47,93 @@ void main() async { } void main() { - testWidgets('Live timeline', (tester) async { - final output = await helpers.captureConsoleOutput(() async { - recordLiveTimeline(); - await tester.pumpWidget(const TimelineTestWidget()); - _addButtonSelector.existsOnce(); - spotText('Counter: 3').existsOnce(); - await act.tap(_addButtonSelector); - spotText('Counter: 4').existsOnce(); - await act.tap(_subtractButtonSelector); - spotText('Counter: 3').existsOnce(); - // Notify that the timeline of this type is already recording. - recordLiveTimeline(); - }); - expect(output, contains('馃敶 - Now recording live timeline')); - expect( - output, - contains('Event: Tap ${_addButtonSelector.toStringBreadcrumb()}'), - ); - expect( - output, - contains('Event: Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), - ); - expect(output, contains('馃敶 - Already recording live timeline')); - _testTimeLineContent(output: output, eventCount: 2); - }); - testWidgets('Start with Timeline Mode off', (tester) async { - final output = await helpers.captureConsoleOutput(() async { - stopRecordingTimeline(); - await tester.pumpWidget(const TimelineTestWidget()); - _addButtonSelector.existsOnce(); - spotText('Counter: 3').existsOnce(); - await act.tap(_addButtonSelector); - spotText('Counter: 4').existsOnce(); - await act.tap(_subtractButtonSelector); - spotText('Counter: 3').existsOnce(); + globalTimelineMode = TimelineMode.off; + group('Manage timeline mode within test', () { + testWidgets('Live timeline', (tester) async { + final output = await helpers.captureConsoleOutput(() async { + recordLiveTimeline(); + await tester.pumpWidget(const TimelineTestWidget()); + _addButtonSelector.existsOnce(); + spotText('Counter: 3').existsOnce(); + await act.tap(_addButtonSelector); + spotText('Counter: 4').existsOnce(); + await act.tap(_subtractButtonSelector); + spotText('Counter: 3').existsOnce(); + // Notify that the timeline of this type is already recording. + recordLiveTimeline(); + }); + expect(output, contains('馃敶 - Now recording live timeline')); + expect( + output, + contains('Event: Tap ${_addButtonSelector.toStringBreadcrumb()}'), + ); + expect( + output, + contains('Event: Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), + ); + expect(output, contains('馃敶 - Already recording live timeline')); + _testTimeLineContent(output: output, eventCount: 2); }); + testWidgets('Start with Timeline Mode off', (tester) async { + final output = await helpers.captureConsoleOutput(() async { + stopRecordingTimeline(); + await tester.pumpWidget(const TimelineTestWidget()); + _addButtonSelector.existsOnce(); + spotText('Counter: 3').existsOnce(); + await act.tap(_addButtonSelector); + spotText('Counter: 4').existsOnce(); + await act.tap(_subtractButtonSelector); + spotText('Counter: 3').existsOnce(); + }); - expect(output, contains('鈴革笌 - Timeline recording is off')); - expect( - output, - isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), - ); - expect( - output, - isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), - ); - _testTimeLineContent(output: output, eventCount: 0); - }); - testWidgets('Turn timeline mode off during test', (tester) async { - final output = await helpers.captureConsoleOutput(() async { - recordLiveTimeline(); - await tester.pumpWidget( - const TimelineTestWidget(), + expect(output, contains('鈴革笌 - Timeline recording is off')); + expect( + output, + isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), ); - spotText('Counter: 3').existsOnce(); - _addButtonSelector.existsOnce(); - await act.tap(_addButtonSelector); - spotText('Counter: 4').existsOnce(); - await act.tap(_subtractButtonSelector); - spotText('Counter: 3').existsOnce(); - // Notify that the recording stopped - stopRecordingTimeline(); - await act.tap(_clearButtonSelector); - spotText('Counter: 0').existsOnce(); - // Notify that the recording is off - stopRecordingTimeline(); + expect( + output, + isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), + ); + _testTimeLineContent(output: output, eventCount: 0); + }); + testWidgets('Turn timeline mode off during test', (tester) async { + final output = await helpers.captureConsoleOutput(() async { + recordLiveTimeline(); + await tester.pumpWidget( + const TimelineTestWidget(), + ); + spotText('Counter: 3').existsOnce(); + _addButtonSelector.existsOnce(); + await act.tap(_addButtonSelector); + spotText('Counter: 4').existsOnce(); + await act.tap(_subtractButtonSelector); + spotText('Counter: 3').existsOnce(); + // Notify that the recording stopped + stopRecordingTimeline(); + await act.tap(_clearButtonSelector); + spotText('Counter: 0').existsOnce(); + // Notify that the recording is off + stopRecordingTimeline(); + }); + expect(output, contains('馃敶 - Now recording live timeline')); + expect( + output, contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')); + expect( + output, + contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), + ); + expect(output, contains('鈴革笌 - Timeline recording stopped')); + // No further events were added to the timeline, including screenshots + expect( + output, + isNot(contains('Tap ${_clearButtonSelector.toStringBreadcrumb()}')), + ); + _testTimeLineContent(output: output, eventCount: 2); + expect(output, contains('鈴革笌 - Timeline recording is off')); }); - expect(output, contains('馃敶 - Now recording live timeline')); - expect(output, contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')); - expect( - output, - contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), - ); - expect(output, contains('鈴革笌 - Timeline recording stopped')); - // No further events were added to the timeline, including screenshots - expect( - output, - isNot(contains('Tap ${_clearButtonSelector.toStringBreadcrumb()}')), - ); - _testTimeLineContent(output: output, eventCount: 2); - expect(output, contains('鈴革笌 - Timeline recording is off')); }); +// globalTimelineMode = defaultTimelineMode(); group('Print on teardown', () { testWidgets('OnError timeline - without error', (tester) async { @@ -396,8 +401,12 @@ void _testTimeLineContent({ RegExp('Event: Tap Icon Widget with icon:').allMatches(output).length, eventCount, ); + final callerParts = output.split('\n').where((line) { + return line.startsWith('Caller: at main') && line.contains('file://'); + }).toList(); + expect( - RegExp('Caller: at main.. file:///').allMatches(output).length, + callerParts.length, eventCount, ); expect( From 429b5beceacf3a121ce12afd6509092a49f2444a Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Tue, 25 Jun 2024 13:10:11 +0200 Subject: [PATCH 086/119] Add test "initial values" and "Override global timeline" --- lib/spot.dart | 3 +- lib/src/timeline/timeline.dart | 67 +++++++++++----------- test/act/act_drag_test.dart | 3 +- test/timeline/drag/timeline_drag_test.dart | 16 +++--- test/timeline/tap/timeline_tap_test.dart | 42 ++++++++------ 5 files changed, 69 insertions(+), 62 deletions(-) diff --git a/lib/spot.dart b/lib/spot.dart index 98ea1744..361e1288 100644 --- a/lib/spot.dart +++ b/lib/spot.dart @@ -92,8 +92,7 @@ export 'package:spot/src/spot/widget_selector.dart' // ignore: deprecated_member_use_from_same_package SingleWidgetSelector, WidgetSelector; -export 'package:spot/src/timeline/timeline.dart' - show recordLiveTimeline, recordOnErrorTimeline, stopRecordingTimeline; +export 'package:spot/src/timeline/timeline.dart'; export 'package:spot/src/widgets/align.g.dart'; export 'package:spot/src/widgets/anytext.g.dart'; export 'package:spot/src/widgets/circularprogressindicator.g.dart'; diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index 939833dd..d947f26a 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -12,33 +12,6 @@ import 'package:test_api/src/backend/invoker.dart'; //ignore: implementation_imports import 'package:test_api/src/backend/live_test.dart'; -/// Records the timeline and prints events as they happen. -void recordLiveTimeline() { - final isRecordingLive = timeline.mode == TimelineMode.live; - final message = isRecordingLive ? 'Already recording' : 'Now recording'; - // ignore: avoid_print - print('馃敶 - $message live timeline'); - timeline.mode = TimelineMode.live; -} - -/// Records the timeline but only prints it in case of an error. -void recordOnErrorTimeline() { - final isRecordingError = timeline.mode == TimelineMode.record; - final message = isRecordingError ? 'Already' : 'Now'; - // ignore: avoid_print - print('馃敶 - $message recording error output timeline'); - timeline.mode = TimelineMode.record; -} - -/// Stops the timeline from recording. -void stopRecordingTimeline() { - final isRecording = timeline.mode != TimelineMode.off; - final message = isRecording ? 'stopped' : 'is off'; - // ignore: avoid_print - print('鈴革笌 - Timeline recording $message'); - timeline.mode = TimelineMode.off; -} - /// Use to set the timeline mode for all tests in a test file. /// /// ```dart @@ -54,11 +27,29 @@ void stopRecordingTimeline() { /// }); /// } /// ``` -TimelineMode globalTimelineMode = defaultTimelineMode(); +TimelineMode globalTimelineMode = + getTimelineModeFromEnv() ?? TimelineMode.record; + +/// ... +TimelineMode? _localTimelineMode; -/// Returns the default timeline mode. -TimelineMode defaultTimelineMode() { - return getTimelineModeFromEnv() ?? TimelineMode.record; +/// Returns the local timeline mode used within a test. +TimelineMode? get localTimelineMode => _localTimelineMode; + +/// Sets the local timeline mode used within a test. +set localTimelineMode(TimelineMode? value) { + if (value != null) { + // ignore: avoid_print + if (_localTimelineMode != null && value == timeline.mode) { + // ignore: avoid_print + print('Timeline mode is already set to "${value.name}"'); + } else { + // ignore: avoid_print + print(value.message); + } + _localTimelineMode = value; + timeline.mode = value; + } } /// Use --dart-define=SPOT_TIMELINE_MODE=live|record|off to set the [TimlineMode] @@ -90,7 +81,7 @@ Timeline get timeline { // create new timeline final newTimeline = Timeline(); - newTimeline.mode = globalTimelineMode; + newTimeline.mode = _localTimelineMode ?? globalTimelineMode; Invoker.current!.addTearDown(() { if (!test.state.result.isPassing && @@ -103,6 +94,7 @@ Timeline get timeline { newTimeline._printHTML(); } _timelines.remove(test); + _localTimelineMode = null; }); _timelines[test] = newTimeline; @@ -431,13 +423,18 @@ class TimelineEvent { /// - [TimelineMode.off] - The timeline is not recording. enum TimelineMode { /// The timeline is recording and printing events as they happen. - live, + live('馃敶 - Recording live timeline'), /// The timeline is recording but not printing events unless the test fails. - record, + record('馃敶 - Recording error output timeline'), /// The timeline is not recording. - off, + off('鈴革笌 - Timeline recording is off'); + + const TimelineMode(this.message); + + /// The message to display when the timeline mode is set. + final String message; } /// Returns the test name including the group hierarchy. diff --git a/test/act/act_drag_test.dart b/test/act/act_drag_test.dart index 303909a7..7c1470e4 100644 --- a/test/act/act_drag_test.dart +++ b/test/act/act_drag_test.dart @@ -1,12 +1,13 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; +import 'package:spot/src/timeline/timeline.dart'; import '../timeline/drag/drag_until_visible_test_widget.dart'; void main() { group('Drag Events', () { testWidgets('Finds widget after dragging', (tester) async { - recordLiveTimeline(); + localTimelineMode = TimelineMode.live; await tester.pumpWidget( const DragUntilVisibleTestWidget(), ); diff --git a/test/timeline/drag/timeline_drag_test.dart b/test/timeline/drag/timeline_drag_test.dart index 9b26f3f6..4e4f6c0d 100644 --- a/test/timeline/drag/timeline_drag_test.dart +++ b/test/timeline/drag/timeline_drag_test.dart @@ -69,10 +69,10 @@ void main() { group('Without error', () { testWidgets('Drag Until Visible - Live timeline', (tester) async { final output = await helpers.captureConsoleOutput(() async { - recordLiveTimeline(); + localTimelineMode = TimelineMode.live; await _testBody(tester); // Notify that the timeline of this type is already recording. - recordLiveTimeline(); + localTimelineMode = TimelineMode.live; }); expect(output, contains('馃敶 - Now recording live timeline')); _testTimeLineContent( @@ -84,7 +84,7 @@ void main() { }); testWidgets('Start with Timeline Mode off', (tester) async { final output = await helpers.captureConsoleOutput(() async { - stopRecordingTimeline(); + localTimelineMode = TimelineMode.off; await _testBody(tester); }); final splitted = output.split('\n') @@ -95,11 +95,11 @@ void main() { }); testWidgets('Turn timeline mode off during test', (tester) async { final output = await helpers.captureConsoleOutput(() async { - recordLiveTimeline(); + localTimelineMode = TimelineMode.record; await _testBody(tester); // Notify that the recording is off - stopRecordingTimeline(); - stopRecordingTimeline(); + localTimelineMode = TimelineMode.off; + localTimelineMode = TimelineMode.off; }); expect(output, contains('馃敶 - Now recording live timeline')); @@ -114,9 +114,9 @@ void main() { }); testWidgets('act.drag: OnError timeline - without error', (tester) async { final output = await helpers.captureConsoleOutput(() async { - recordOnErrorTimeline(); + localTimelineMode = TimelineMode.record; await _testBody(tester); - recordOnErrorTimeline(); + localTimelineMode = TimelineMode.record; }); final lines = output.split('\n')..removeWhere((line) => line.isEmpty); expect(lines.first, '馃敶 - Now recording error output timeline'); diff --git a/test/timeline/tap/timeline_tap_test.dart b/test/timeline/tap/timeline_tap_test.dart index 10ddd62a..60ba6fc2 100644 --- a/test/timeline/tap/timeline_tap_test.dart +++ b/test/timeline/tap/timeline_tap_test.dart @@ -47,11 +47,19 @@ void main() async { } void main() { - globalTimelineMode = TimelineMode.off; - group('Manage timeline mode within test', () { + group('Initial Values', () { + test('global timeline', () { + expect(globalTimelineMode, TimelineMode.record); + }); + test('local timeline', () { + expect(localTimelineMode, isNull); + }); + }); + + group('Override global timeline', () { testWidgets('Live timeline', (tester) async { final output = await helpers.captureConsoleOutput(() async { - recordLiveTimeline(); + localTimelineMode = TimelineMode.live; await tester.pumpWidget(const TimelineTestWidget()); _addButtonSelector.existsOnce(); spotText('Counter: 3').existsOnce(); @@ -59,10 +67,11 @@ void main() { spotText('Counter: 4').existsOnce(); await act.tap(_subtractButtonSelector); spotText('Counter: 3').existsOnce(); - // Notify that the timeline of this type is already recording. - recordLiveTimeline(); + // Notify that the timeline mode is already set to live + localTimelineMode = TimelineMode.live; }); - expect(output, contains('馃敶 - Now recording live timeline')); + print('output: $output'); + expect(output, contains('馃敶 - Recording live timeline')); expect( output, contains('Event: Tap ${_addButtonSelector.toStringBreadcrumb()}'), @@ -71,12 +80,12 @@ void main() { output, contains('Event: Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), ); - expect(output, contains('馃敶 - Already recording live timeline')); + expect(output, contains('Timeline mode is already set to "live"')); _testTimeLineContent(output: output, eventCount: 2); }); testWidgets('Start with Timeline Mode off', (tester) async { final output = await helpers.captureConsoleOutput(() async { - stopRecordingTimeline(); + localTimelineMode = TimelineMode.off; await tester.pumpWidget(const TimelineTestWidget()); _addButtonSelector.existsOnce(); spotText('Counter: 3').existsOnce(); @@ -99,7 +108,7 @@ void main() { }); testWidgets('Turn timeline mode off during test', (tester) async { final output = await helpers.captureConsoleOutput(() async { - recordLiveTimeline(); + localTimelineMode = TimelineMode.live; await tester.pumpWidget( const TimelineTestWidget(), ); @@ -110,20 +119,20 @@ void main() { await act.tap(_subtractButtonSelector); spotText('Counter: 3').existsOnce(); // Notify that the recording stopped - stopRecordingTimeline(); + localTimelineMode = TimelineMode.off; await act.tap(_clearButtonSelector); spotText('Counter: 0').existsOnce(); - // Notify that the recording is off - stopRecordingTimeline(); + // Notify that the recording is already off + localTimelineMode = TimelineMode.off; }); - expect(output, contains('馃敶 - Now recording live timeline')); + print('output: $output'); + expect(output, contains('馃敶 - Recording live timeline')); expect( output, contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')); expect( output, contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), ); - expect(output, contains('鈴革笌 - Timeline recording stopped')); // No further events were added to the timeline, including screenshots expect( output, @@ -131,6 +140,7 @@ void main() { ); _testTimeLineContent(output: output, eventCount: 2); expect(output, contains('鈴革笌 - Timeline recording is off')); + expect(output, contains('Timeline mode is already set to "off"')); }); }); // globalTimelineMode = defaultTimelineMode(); @@ -138,7 +148,7 @@ void main() { group('Print on teardown', () { testWidgets('OnError timeline - without error', (tester) async { final output = await helpers.captureConsoleOutput(() async { - recordOnErrorTimeline(); + localTimelineMode = TimelineMode.record; await tester.pumpWidget(const TimelineTestWidget()); _addButtonSelector.existsOnce(); spotText('Counter: 3').existsOnce(); @@ -147,7 +157,7 @@ void main() { await act.tap(_subtractButtonSelector); spotText('Counter: 3').existsOnce(); // Notify that the timeline of this type is already recording. - recordOnErrorTimeline(); + localTimelineMode = TimelineMode.record; }); expect(output, contains('馃敶 - Now recording error output timeline')); expect( From 9ce989f293414cd93dfdc74ba1b58bc319f5b78d Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Tue, 25 Jun 2024 16:28:26 +0200 Subject: [PATCH 087/119] Adjust group "Print on teardown" to new timeline handling --- lib/src/act/act.dart | 1 - lib/src/timeline/timeline.dart | 11 +++++++---- test/act/act_drag_test.dart | 1 - test/timeline/drag/timeline_drag_test.dart | 1 - test/timeline/tap/timeline_tap_test.dart | 12 +++++------- test/util/timeline_test_helpers.dart | 6 +++--- 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/lib/src/act/act.dart b/lib/src/act/act.dart index 40bd3189..3770094a 100644 --- a/lib/src/act/act.dart +++ b/lib/src/act/act.dart @@ -7,7 +7,6 @@ import 'package:spot/spot.dart'; import 'package:spot/src/act/gestures.dart'; import 'package:spot/src/screenshot/screenshot.dart'; import 'package:spot/src/spot/snapshot.dart'; -import 'package:spot/src/timeline/timeline.dart'; /// Top level entry point to interact with widgets on the screen. /// diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index d947f26a..c2009168 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -38,16 +38,17 @@ TimelineMode? get localTimelineMode => _localTimelineMode; /// Sets the local timeline mode used within a test. set localTimelineMode(TimelineMode? value) { + final currentTimelineMode = _localTimelineMode; + _localTimelineMode = value; if (value != null) { // ignore: avoid_print - if (_localTimelineMode != null && value == timeline.mode) { + if (currentTimelineMode != null && value == timeline.mode) { // ignore: avoid_print print('Timeline mode is already set to "${value.name}"'); - } else { + } else if (currentTimelineMode != null && currentTimelineMode != value) { // ignore: avoid_print print(value.message); } - _localTimelineMode = value; timeline.mode = value; } } @@ -82,6 +83,8 @@ Timeline get timeline { // create new timeline final newTimeline = Timeline(); newTimeline.mode = _localTimelineMode ?? globalTimelineMode; + // ignore: avoid_print + print(newTimeline.mode.message); Invoker.current!.addTearDown(() { if (!test.state.result.isPassing && @@ -358,7 +361,7 @@ class TimelineEventType { final String label; // TODO add styling information like color? - + /// Creates a new timeline event type. const TimelineEventType({ required this.label, }); diff --git a/test/act/act_drag_test.dart b/test/act/act_drag_test.dart index 7c1470e4..c545d92a 100644 --- a/test/act/act_drag_test.dart +++ b/test/act/act_drag_test.dart @@ -1,6 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; -import 'package:spot/src/timeline/timeline.dart'; import '../timeline/drag/drag_until_visible_test_widget.dart'; diff --git a/test/timeline/drag/timeline_drag_test.dart b/test/timeline/drag/timeline_drag_test.dart index 4e4f6c0d..0bafe320 100644 --- a/test/timeline/drag/timeline_drag_test.dart +++ b/test/timeline/drag/timeline_drag_test.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'package:dartx/dartx.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; -import 'package:spot/src/timeline/timeline.dart'; import 'package:test_process/test_process.dart'; import '../../util/timeline_test_helpers.dart' as helpers; import 'drag_until_visible_test_widget.dart'; diff --git a/test/timeline/tap/timeline_tap_test.dart b/test/timeline/tap/timeline_tap_test.dart index 60ba6fc2..cd218533 100644 --- a/test/timeline/tap/timeline_tap_test.dart +++ b/test/timeline/tap/timeline_tap_test.dart @@ -4,7 +4,6 @@ import 'package:dartx/dartx.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; -import 'package:spot/src/timeline/timeline.dart'; import 'package:test_process/test_process.dart'; import '../../util/timeline_test_helpers.dart' as helpers; import 'timeline_tap_test_widget.dart'; @@ -70,7 +69,6 @@ void main() { // Notify that the timeline mode is already set to live localTimelineMode = TimelineMode.live; }); - print('output: $output'); expect(output, contains('馃敶 - Recording live timeline')); expect( output, @@ -125,10 +123,11 @@ void main() { // Notify that the recording is already off localTimelineMode = TimelineMode.off; }); - print('output: $output'); expect(output, contains('馃敶 - Recording live timeline')); expect( - output, contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')); + output, + contains('Tap ${_addButtonSelector.toStringBreadcrumb()}'), + ); expect( output, contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), @@ -143,7 +142,6 @@ void main() { expect(output, contains('Timeline mode is already set to "off"')); }); }); -// globalTimelineMode = defaultTimelineMode(); group('Print on teardown', () { testWidgets('OnError timeline - without error', (tester) async { @@ -159,7 +157,7 @@ void main() { // Notify that the timeline of this type is already recording. localTimelineMode = TimelineMode.record; }); - expect(output, contains('馃敶 - Now recording error output timeline')); + expect(output, contains('馃敶 - Recording error output timeline')); expect( output, isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), @@ -168,7 +166,7 @@ void main() { output, isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), ); - expect(output, contains('馃敶 - Already recording error output timeline')); + expect(output, contains('Timeline mode is already set to "record"')); _testTimeLineContent(output: output, eventCount: 0); }); diff --git a/test/util/timeline_test_helpers.dart b/test/util/timeline_test_helpers.dart index f5d83693..aa6cae30 100644 --- a/test/util/timeline_test_helpers.dart +++ b/test/util/timeline_test_helpers.dart @@ -22,10 +22,10 @@ Future captureConsoleOutput( String timelineInitiatorForModeAsString(TimelineMode timelineMode) { switch (timelineMode) { case TimelineMode.live: - return 'recordLiveTimeline()'; + return 'localTimelineMode = TimelineMode.live;'; case TimelineMode.record: - return 'recordOnErrorTimeline()'; + return 'localTimelineMode = TimelineMode.record;'; case TimelineMode.off: - return 'stopRecordingTimeline()'; + return 'localTimelineMode = TimelineMode.off;'; } } From 3e43a74befe6a8d8d10fa4064a1cbaba9b6010cc Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 26 Jun 2024 01:43:54 +0200 Subject: [PATCH 088/119] Add test "Global timeline - record, without error" --- .../tap/global/record_timeline_tap_test.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 test/timeline/tap/global/record_timeline_tap_test.dart diff --git a/test/timeline/tap/global/record_timeline_tap_test.dart b/test/timeline/tap/global/record_timeline_tap_test.dart new file mode 100644 index 00000000..bf7b4871 --- /dev/null +++ b/test/timeline/tap/global/record_timeline_tap_test.dart @@ -0,0 +1,14 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/src/timeline/timeline.dart'; + +import '../../../util/timeline_test_helpers.dart'; + +void main() { + globalTimelineMode = TimelineMode.record; + testWidgets('Global timeline - record, without error', (tester) async { + await TimelineTestHelpers.recordTimelineTest( + tester: tester, + isGlobalMode: true, + ); + }); +} From 4f0a9399a7d0e828ed714e9115cd934ffc30ba78 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 26 Jun 2024 01:44:08 +0200 Subject: [PATCH 089/119] Add test "Global timeline - Timeline off" --- .../timeline/tap/global/off_timeline_tap_test.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 test/timeline/tap/global/off_timeline_tap_test.dart diff --git a/test/timeline/tap/global/off_timeline_tap_test.dart b/test/timeline/tap/global/off_timeline_tap_test.dart new file mode 100644 index 00000000..1c89b267 --- /dev/null +++ b/test/timeline/tap/global/off_timeline_tap_test.dart @@ -0,0 +1,14 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/src/timeline/timeline.dart'; + +import '../../../util/timeline_test_helpers.dart'; + +void main() { + globalTimelineMode = TimelineMode.off; + testWidgets('Global timeline - Timeline off', (tester) async { + await TimelineTestHelpers.offTimelineTest( + tester: tester, + isGlobalMode: true, + ); + }); +} From c5dea696470b6a967e7731324fcf5a36edfebf4f Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 26 Jun 2024 01:44:57 +0200 Subject: [PATCH 090/119] Add setter to globaltimeline mode --- lib/src/timeline/timeline.dart | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index c2009168..f689e23e 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -27,9 +27,25 @@ import 'package:test_api/src/backend/live_test.dart'; /// }); /// } /// ``` -TimelineMode globalTimelineMode = +TimelineMode _globalTimelineMode = getTimelineModeFromEnv() ?? TimelineMode.record; +/// Returns the global timeline mode than can be used across multiple tests +TimelineMode get globalTimelineMode => _globalTimelineMode; + +set globalTimelineMode(TimelineMode value) { + // ignore: avoid_print + if (value == _globalTimelineMode) { + // ignore: avoid_print + print('Timeline mode is already set to "${value.name}"'); + return; + } else if (_globalTimelineMode != value) { + // ignore: avoid_print + print(value.message); + } + _globalTimelineMode = value; +} + /// ... TimelineMode? _localTimelineMode; @@ -42,9 +58,11 @@ set localTimelineMode(TimelineMode? value) { _localTimelineMode = value; if (value != null) { // ignore: avoid_print - if (currentTimelineMode != null && value == timeline.mode) { + if (value == globalTimelineMode || + (currentTimelineMode != null && value == timeline.mode)) { // ignore: avoid_print print('Timeline mode is already set to "${value.name}"'); + return; } else if (currentTimelineMode != null && currentTimelineMode != value) { // ignore: avoid_print print(value.message); @@ -82,7 +100,7 @@ Timeline get timeline { // create new timeline final newTimeline = Timeline(); - newTimeline.mode = _localTimelineMode ?? globalTimelineMode; + newTimeline.mode = _localTimelineMode ?? _globalTimelineMode; // ignore: avoid_print print(newTimeline.mode.message); From c5fbee1e35d083f372fce01bd92ac8bc1291737b Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 26 Jun 2024 01:46:19 +0200 Subject: [PATCH 091/119] Add class TimelineTestHelpers and refactor test bodies --- test/timeline/drag/timeline_drag_test.dart | 12 +- .../tap/{ => local}/timeline_tap_test.dart | 79 +------- test/util/timeline_test_helpers.dart | 188 +++++++++++++++--- 3 files changed, 174 insertions(+), 105 deletions(-) rename test/timeline/tap/{ => local}/timeline_tap_test.dart (76%) diff --git a/test/timeline/drag/timeline_drag_test.dart b/test/timeline/drag/timeline_drag_test.dart index 0bafe320..b114aa76 100644 --- a/test/timeline/drag/timeline_drag_test.dart +++ b/test/timeline/drag/timeline_drag_test.dart @@ -4,7 +4,7 @@ import 'package:dartx/dartx.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import 'package:test_process/test_process.dart'; -import '../../util/timeline_test_helpers.dart' as helpers; +import '../../util/timeline_test_helpers.dart'; import 'drag_until_visible_test_widget.dart'; final _firstItemSelector = spotText('Item at index: 3', exact: true); @@ -33,7 +33,7 @@ import 'package:spot/src/timeline/timeline.dart';\n $widgetPart\n void main() async { testWidgets("$title", (WidgetTester tester) async { - ${helpers.timelineInitiatorForModeAsString(timelineMode)}; + ${TimelineTestHelpers.timelineInitiatorForModeAsString(timelineMode)}; await tester.pumpWidget(const DragUntilVisibleTestWidget()); final firstItem = spotText('Item at index: 3', exact: true)..existsOnce(); final secondItem = spotText('Item at index: 27', exact: true) @@ -67,7 +67,7 @@ void main() { group('Drag Timeline Test', () { group('Without error', () { testWidgets('Drag Until Visible - Live timeline', (tester) async { - final output = await helpers.captureConsoleOutput(() async { + final output = await TimelineTestHelpers.captureConsoleOutput(() async { localTimelineMode = TimelineMode.live; await _testBody(tester); // Notify that the timeline of this type is already recording. @@ -82,7 +82,7 @@ void main() { expect(output, contains('馃敶 - Already recording live timeline')); }); testWidgets('Start with Timeline Mode off', (tester) async { - final output = await helpers.captureConsoleOutput(() async { + final output = await TimelineTestHelpers.captureConsoleOutput(() async { localTimelineMode = TimelineMode.off; await _testBody(tester); }); @@ -93,7 +93,7 @@ void main() { expect(splitted.first, expectedOutput); }); testWidgets('Turn timeline mode off during test', (tester) async { - final output = await helpers.captureConsoleOutput(() async { + final output = await TimelineTestHelpers.captureConsoleOutput(() async { localTimelineMode = TimelineMode.record; await _testBody(tester); // Notify that the recording is off @@ -112,7 +112,7 @@ void main() { expect(output, contains('鈴革笌 - Timeline recording is off')); }); testWidgets('act.drag: OnError timeline - without error', (tester) async { - final output = await helpers.captureConsoleOutput(() async { + final output = await TimelineTestHelpers.captureConsoleOutput(() async { localTimelineMode = TimelineMode.record; await _testBody(tester); localTimelineMode = TimelineMode.record; diff --git a/test/timeline/tap/timeline_tap_test.dart b/test/timeline/tap/local/timeline_tap_test.dart similarity index 76% rename from test/timeline/tap/timeline_tap_test.dart rename to test/timeline/tap/local/timeline_tap_test.dart index cd218533..0d354bde 100644 --- a/test/timeline/tap/timeline_tap_test.dart +++ b/test/timeline/tap/local/timeline_tap_test.dart @@ -5,8 +5,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import 'package:test_process/test_process.dart'; -import '../../util/timeline_test_helpers.dart' as helpers; -import 'timeline_tap_test_widget.dart'; +import '../../../util/timeline_test_helpers.dart'; +import '../timeline_tap_test_widget.dart'; final _addButtonSelector = spotIcon(Icons.add); final _subtractButtonSelector = spotIcon(Icons.remove); @@ -31,7 +31,7 @@ void main() async { final addButtonSelector = spotIcon(Icons.add); final subtractButtonSelector = spotIcon(Icons.remove); testWidgets("$title", (WidgetTester tester) async { - ${helpers.timelineInitiatorForModeAsString(timelineMode)}; + ${TimelineTestHelpers.timelineInitiatorForModeAsString(timelineMode)}; await tester.pumpWidget(const TimelineTestWidget()); addButtonSelector.existsOnce(); spotText('Counter: 3').existsOnce(); @@ -56,56 +56,11 @@ void main() { }); group('Override global timeline', () { - testWidgets('Live timeline', (tester) async { - final output = await helpers.captureConsoleOutput(() async { - localTimelineMode = TimelineMode.live; - await tester.pumpWidget(const TimelineTestWidget()); - _addButtonSelector.existsOnce(); - spotText('Counter: 3').existsOnce(); - await act.tap(_addButtonSelector); - spotText('Counter: 4').existsOnce(); - await act.tap(_subtractButtonSelector); - spotText('Counter: 3').existsOnce(); - // Notify that the timeline mode is already set to live - localTimelineMode = TimelineMode.live; - }); - expect(output, contains('馃敶 - Recording live timeline')); - expect( - output, - contains('Event: Tap ${_addButtonSelector.toStringBreadcrumb()}'), - ); - expect( - output, - contains('Event: Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), - ); - expect(output, contains('Timeline mode is already set to "live"')); - _testTimeLineContent(output: output, eventCount: 2); - }); testWidgets('Start with Timeline Mode off', (tester) async { - final output = await helpers.captureConsoleOutput(() async { - localTimelineMode = TimelineMode.off; - await tester.pumpWidget(const TimelineTestWidget()); - _addButtonSelector.existsOnce(); - spotText('Counter: 3').existsOnce(); - await act.tap(_addButtonSelector); - spotText('Counter: 4').existsOnce(); - await act.tap(_subtractButtonSelector); - spotText('Counter: 3').existsOnce(); - }); - - expect(output, contains('鈴革笌 - Timeline recording is off')); - expect( - output, - isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), - ); - expect( - output, - isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), - ); - _testTimeLineContent(output: output, eventCount: 0); + TimelineTestHelpers.offTimelineTest(tester: tester); }); testWidgets('Turn timeline mode off during test', (tester) async { - final output = await helpers.captureConsoleOutput(() async { + final output = await TimelineTestHelpers.captureConsoleOutput(() async { localTimelineMode = TimelineMode.live; await tester.pumpWidget( const TimelineTestWidget(), @@ -145,29 +100,7 @@ void main() { group('Print on teardown', () { testWidgets('OnError timeline - without error', (tester) async { - final output = await helpers.captureConsoleOutput(() async { - localTimelineMode = TimelineMode.record; - await tester.pumpWidget(const TimelineTestWidget()); - _addButtonSelector.existsOnce(); - spotText('Counter: 3').existsOnce(); - await act.tap(_addButtonSelector); - spotText('Counter: 4').existsOnce(); - await act.tap(_subtractButtonSelector); - spotText('Counter: 3').existsOnce(); - // Notify that the timeline of this type is already recording. - localTimelineMode = TimelineMode.record; - }); - expect(output, contains('馃敶 - Recording error output timeline')); - expect( - output, - isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), - ); - expect( - output, - isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), - ); - expect(output, contains('Timeline mode is already set to "record"')); - _testTimeLineContent(output: output, eventCount: 0); + await TimelineTestHelpers.recordTimelineTest(tester: tester); }); test('OnError timeline - with error, prints timeline', () async { diff --git a/test/util/timeline_test_helpers.dart b/test/util/timeline_test_helpers.dart index aa6cae30..df1291a0 100644 --- a/test/util/timeline_test_helpers.dart +++ b/test/util/timeline_test_helpers.dart @@ -1,31 +1,167 @@ import 'dart:async'; -import 'package:spot/src/timeline/timeline.dart'; - -Future captureConsoleOutput( - Future Function() testFunction, -) async { - final StringBuffer buffer = StringBuffer(); - final ZoneSpecification spec = ZoneSpecification( - print: (self, parent, zone, line) { - buffer.writeln(line); - }, - ); - - await Zone.current.fork(specification: spec).run(() async { - await testFunction(); - }); - - return buffer.toString(); -} +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; + +import '../timeline/tap/timeline_tap_test_widget.dart'; + +class TimelineTestHelpers { + static final WidgetSelector addButtonSelector = spotIcon(Icons.add); + static final subtractButtonSelector = spotIcon(Icons.remove); + static final clearButtonSelector = spotIcon(Icons.clear); + + static const header = + '==================== Timeline Event ===================='; + static const separator = + '========================================================'; + + static Future captureConsoleOutput( + Future Function() testFunction, + ) async { + final StringBuffer buffer = StringBuffer(); + final ZoneSpecification spec = ZoneSpecification( + print: (self, parent, zone, line) { + buffer.writeln(line); + }, + ); + + await Zone.current.fork(specification: spec).run(() async { + await testFunction(); + }); + + return buffer.toString(); + } + + static String timelineInitiatorForModeAsString(TimelineMode timelineMode) { + switch (timelineMode) { + case TimelineMode.live: + return 'localTimelineMode = TimelineMode.live;'; + case TimelineMode.record: + return 'localTimelineMode = TimelineMode.record;'; + case TimelineMode.off: + return 'localTimelineMode = TimelineMode.off;'; + } + } + + static Future recordTimelineTest({ + required WidgetTester tester, + bool isGlobalMode = false, + }) async { + final output = await TimelineTestHelpers.captureConsoleOutput(() async { + if (!isGlobalMode) { + localTimelineMode = TimelineMode.record; + } + await tester.pumpWidget(const TimelineTestWidget()); + addButtonSelector.existsOnce(); + spotText('Counter: 3').existsOnce(); + await act.tap(addButtonSelector); + spotText('Counter: 4').existsOnce(); + await act.tap(subtractButtonSelector); + spotText('Counter: 3').existsOnce(); + // Notify that the timeline of this type is already recording. + localTimelineMode = TimelineMode.record; + }); + expect(output, contains('馃敶 - Recording error output timeline')); + expect( + output, + isNot(contains('Tap ${addButtonSelector.toStringBreadcrumb()}')), + ); + expect( + output, + isNot(contains('Tap ${subtractButtonSelector.toStringBreadcrumb()}')), + ); + expect(output, contains('Timeline mode is already set to "record"')); + _testTimeLineContent(output: output, eventCount: 0); + } + + static Future offTimelineTest({ + required WidgetTester tester, + bool isGlobalMode = false, + }) async { + final output = await TimelineTestHelpers.captureConsoleOutput(() async { + if (!isGlobalMode) { + localTimelineMode = TimelineMode.off; + } + await tester.pumpWidget(const TimelineTestWidget()); + addButtonSelector.existsOnce(); + spotText('Counter: 3').existsOnce(); + await act.tap(addButtonSelector); + spotText('Counter: 4').existsOnce(); + await act.tap(subtractButtonSelector); + spotText('Counter: 3').existsOnce(); + }); + + expect(output, contains('鈴革笌 - Timeline recording is off')); + expect( + output, + isNot(contains('Tap ${addButtonSelector.toStringBreadcrumb()}')), + ); + expect( + output, + isNot(contains('Tap ${subtractButtonSelector.toStringBreadcrumb()}')), + ); + _testTimeLineContent(output: output, eventCount: 0); + } + + static Future liveTimelineTest({ + required WidgetTester tester, + bool isGlobalMode = false, + }) async { + final output = await TimelineTestHelpers.captureConsoleOutput(() async { + if (!isGlobalMode) { + localTimelineMode = TimelineMode.live; + } + await tester.pumpWidget(const TimelineTestWidget()); + addButtonSelector.existsOnce(); + spotText('Counter: 3').existsOnce(); + await act.tap(addButtonSelector); + spotText('Counter: 4').existsOnce(); + await act.tap(subtractButtonSelector); + spotText('Counter: 3').existsOnce(); + // Notify that the timeline mode is already set to live + localTimelineMode = TimelineMode.live; + }); + expect(output, contains('馃敶 - Recording live timeline')); + expect( + output, + contains('Event: Tap ${addButtonSelector.toStringBreadcrumb()}'), + ); + expect( + output, + contains('Event: Tap ${subtractButtonSelector.toStringBreadcrumb()}'), + ); + expect(output, contains('Timeline mode is already set to "live"')); + _testTimeLineContent(output: output, eventCount: 2); + } + + static void _testTimeLineContent({ + required String output, + required int eventCount, + }) { + expect( + RegExp(header).allMatches(output).length, + eventCount, + ); + expect( + RegExp('Event: Tap Icon Widget with icon:').allMatches(output).length, + eventCount, + ); + final callerParts = output.split('\n').where((line) { + return line.startsWith('Caller: at main') && line.contains('file://'); + }).toList(); -String timelineInitiatorForModeAsString(TimelineMode timelineMode) { - switch (timelineMode) { - case TimelineMode.live: - return 'localTimelineMode = TimelineMode.live;'; - case TimelineMode.record: - return 'localTimelineMode = TimelineMode.record;'; - case TimelineMode.off: - return 'localTimelineMode = TimelineMode.off;'; + expect( + callerParts.length, + eventCount, + ); + expect( + RegExp('Screenshot: file:').allMatches(output).length, + eventCount, + ); + expect( + RegExp('Timestamp: ').allMatches(output).length, + eventCount, + ); } } From 9733d70adaa93e67b4e704c307992a56ce53ce53 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 26 Jun 2024 03:05:45 +0200 Subject: [PATCH 092/119] Add test "Global: Live timeline - without error, prints HTML" --- test/timeline/drag/timeline_drag_test.dart | 2 +- .../timeline/tap/global/live_timeline_tap_test.dart | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 test/timeline/tap/global/live_timeline_tap_test.dart diff --git a/test/timeline/drag/timeline_drag_test.dart b/test/timeline/drag/timeline_drag_test.dart index b114aa76..e0b5a918 100644 --- a/test/timeline/drag/timeline_drag_test.dart +++ b/test/timeline/drag/timeline_drag_test.dart @@ -33,7 +33,7 @@ import 'package:spot/src/timeline/timeline.dart';\n $widgetPart\n void main() async { testWidgets("$title", (WidgetTester tester) async { - ${TimelineTestHelpers.timelineInitiatorForModeAsString(timelineMode)}; + ${TimelineTestHelpers.localTimelineInitiator(timelineMode)}; await tester.pumpWidget(const DragUntilVisibleTestWidget()); final firstItem = spotText('Item at index: 3', exact: true)..existsOnce(); final secondItem = spotText('Item at index: 27', exact: true) diff --git a/test/timeline/tap/global/live_timeline_tap_test.dart b/test/timeline/tap/global/live_timeline_tap_test.dart new file mode 100644 index 00000000..a0f442c4 --- /dev/null +++ b/test/timeline/tap/global/live_timeline_tap_test.dart @@ -0,0 +1,13 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/src/timeline/timeline.dart'; + +import '../../../util/timeline_test_helpers.dart'; + +void main() { + globalTimelineMode = TimelineMode.live; + test('Global: Live timeline - without error, prints HTML', () async { + await TimelineTestHelpers.liveTimelineWithoutErrorPrintsHtml( + isGlobalMode: true, + ); + }); +} From 907b47c5e226935fd36779938cdee8e738e46d9a Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 26 Jun 2024 03:06:09 +0200 Subject: [PATCH 093/119] Add test "Global timeline - record, with error" --- .../tap/global/record_timeline_tap_test.dart | 5 +- .../timeline/tap/local/timeline_tap_test.dart | 151 +----------- test/util/timeline_test_helpers.dart | 220 +++++++++++++++++- 3 files changed, 226 insertions(+), 150 deletions(-) diff --git a/test/timeline/tap/global/record_timeline_tap_test.dart b/test/timeline/tap/global/record_timeline_tap_test.dart index bf7b4871..24aa26ac 100644 --- a/test/timeline/tap/global/record_timeline_tap_test.dart +++ b/test/timeline/tap/global/record_timeline_tap_test.dart @@ -6,9 +6,12 @@ import '../../../util/timeline_test_helpers.dart'; void main() { globalTimelineMode = TimelineMode.record; testWidgets('Global timeline - record, without error', (tester) async { - await TimelineTestHelpers.recordTimelineTest( + await TimelineTestHelpers.recordTimelineTestWithoutError( tester: tester, isGlobalMode: true, ); }); + test('Global timeline - record, with error', () async { + await TimelineTestHelpers.recordTimelineTestWithError(isGlobalMode: true); + }); } diff --git a/test/timeline/tap/local/timeline_tap_test.dart b/test/timeline/tap/local/timeline_tap_test.dart index 0d354bde..879f9f6f 100644 --- a/test/timeline/tap/local/timeline_tap_test.dart +++ b/test/timeline/tap/local/timeline_tap_test.dart @@ -31,7 +31,7 @@ void main() async { final addButtonSelector = spotIcon(Icons.add); final subtractButtonSelector = spotIcon(Icons.remove); testWidgets("$title", (WidgetTester tester) async { - ${TimelineTestHelpers.timelineInitiatorForModeAsString(timelineMode)}; + ${TimelineTestHelpers.localTimelineInitiator(timelineMode)}; await tester.pumpWidget(const TimelineTestWidget()); addButtonSelector.existsOnce(); spotText('Counter: 3').existsOnce(); @@ -100,157 +100,14 @@ void main() { group('Print on teardown', () { testWidgets('OnError timeline - without error', (tester) async { - await TimelineTestHelpers.recordTimelineTest(tester: tester); + await TimelineTestHelpers.recordTimelineTestWithoutError(tester: tester); }); test('OnError timeline - with error, prints timeline', () async { - final tempDir = Directory.systemTemp.createTempSync(); - final tempTestFile = File('${tempDir.path}/temp_test.dart'); - await tempTestFile.writeAsString( - _testAsString( - title: 'OnError timeline - with error, prints timeline', - timelineMode: TimelineMode.record, - shouldFail: true, - ), - ); - - final testProcess = - await TestProcess.start('flutter', ['test', tempTestFile.path]); - - final stdoutBuffer = StringBuffer(); - - bool write = false; - await for (final line in testProcess.stdoutStream()) { - if (line.isEmpty) continue; - if (line == 'Timeline') { - write = true; - } - if (write) { - stdoutBuffer.writeln(line); - } - } - - // Error happens - await testProcess.shouldExit(1); - - if (tempDir.existsSync()) { - tempDir.deleteSync(recursive: true); - } - - final stdout = stdoutBuffer.toString(); - final timeline = stdout.split('\n'); - - expect(timeline.first, 'Timeline'); - expect( - timeline[1], - _header, - ); - expect( - timeline[2], - 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', - ); - expect( - timeline[3].startsWith('Caller: at main. file:///'), - isTrue, - ); - expect( - timeline[4].startsWith( - 'Screenshot: file:///', - ), - isTrue, - ); - expect( - timeline[5].startsWith( - 'Timestamp:', - ), - isTrue, - ); - expect( - timeline[6], - _separator, - ); - final htmlLine = timeline - .firstWhere((line) => line.startsWith('View time line here:')); - expect( - htmlLine.endsWith( - 'timeline-onerror-timeline-with-error-prints-timeline.html', - ), - isTrue, - ); + await TimelineTestHelpers.recordTimelineTestWithError(); }); test('Live timeline - without error, prints HTML', () async { - final tempDir = Directory.systemTemp.createTempSync(); - final tempTestFile = File('${tempDir.path}/temp_test.dart'); - await tempTestFile.writeAsString( - _testAsString( - title: 'Live timeline without error prints html', - timelineMode: TimelineMode.live, - ), - ); - - final testProcess = - await TestProcess.start('flutter', ['test', tempTestFile.path]); - - final stdoutBuffer = StringBuffer(); - - bool write = false; - await for (final line in testProcess.stdoutStream()) { - if (line.isEmpty) continue; - - if (!write) { - if (line == _header) { - write = true; - } - } - - if (write) { - stdoutBuffer.writeln(line); - } - } - - // Error does not happen - await testProcess.shouldExit(0); - - if (tempDir.existsSync()) { - tempDir.deleteSync(recursive: true); - } - - final stdout = stdoutBuffer.toString(); - final timeline = stdout.split('\n'); - // Does not start with 'Timeline', this only happens on error - expect(timeline.first, _header); - expect( - timeline.second, - 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', - ); - expect( - timeline[2].startsWith('Caller: at main. file:///'), - isTrue, - ); - expect( - timeline[3].startsWith( - 'Screenshot: file:///', - ), - isTrue, - ); - expect( - timeline[4].startsWith( - 'Timestamp:', - ), - isTrue, - ); - expect( - timeline[5], - _separator, - ); - final htmlLine = timeline - .firstWhere((line) => line.startsWith('View time line here:')); - expect( - htmlLine.endsWith( - 'timeline-live-timeline-without-error-prints-html.html', - ), - isTrue, - ); + await TimelineTestHelpers.liveTimelineWithoutErrorPrintsHtml(); }); test('Live timeline - with error, no duplicates, prints HTML', () async { final tempDir = Directory.systemTemp.createTempSync(); diff --git a/test/util/timeline_test_helpers.dart b/test/util/timeline_test_helpers.dart index df1291a0..d6abd47d 100644 --- a/test/util/timeline_test_helpers.dart +++ b/test/util/timeline_test_helpers.dart @@ -1,8 +1,11 @@ import 'dart:async'; +import 'dart:io'; +import 'package:dartx/dartx.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; +import 'package:test_process/test_process.dart'; import '../timeline/tap/timeline_tap_test_widget.dart'; @@ -33,7 +36,7 @@ class TimelineTestHelpers { return buffer.toString(); } - static String timelineInitiatorForModeAsString(TimelineMode timelineMode) { + static String localTimelineInitiator(TimelineMode timelineMode) { switch (timelineMode) { case TimelineMode.live: return 'localTimelineMode = TimelineMode.live;'; @@ -44,7 +47,18 @@ class TimelineTestHelpers { } } - static Future recordTimelineTest({ + static String globalTimelineInitiator(TimelineMode timelineMode) { + switch (timelineMode) { + case TimelineMode.live: + return 'globalTimelineMode = TimelineMode.live;'; + case TimelineMode.record: + return 'globalTimelineMode = TimelineMode.record;'; + case TimelineMode.off: + return 'globalTimelineMode = TimelineMode.off;'; + } + } + + static Future recordTimelineTestWithoutError({ required WidgetTester tester, bool isGlobalMode = false, }) async { @@ -75,6 +89,169 @@ class TimelineTestHelpers { _testTimeLineContent(output: output, eventCount: 0); } + static Future recordTimelineTestWithError({ + bool isGlobalMode = false, + }) async { + final tempDir = Directory.systemTemp.createTempSync(); + final tempTestFile = File('${tempDir.path}/temp_test.dart'); + final testTitle = + '${isGlobalMode ? 'Global: ' : 'Local: '}OnError timeline - with error, prints timeline'; + await tempTestFile.writeAsString( + testAsString( + title: testTitle, + timelineMode: TimelineMode.record, + shouldFail: true, + isGlobalMode: isGlobalMode, + ), + ); + + final testProcess = + await TestProcess.start('flutter', ['test', tempTestFile.path]); + + final stdoutBuffer = StringBuffer(); + + bool write = false; + await for (final line in testProcess.stdoutStream()) { + if (line.isEmpty) continue; + if (line == 'Timeline') { + write = true; + } + if (write) { + stdoutBuffer.writeln(line); + } + } + + // Error happens + await testProcess.shouldExit(1); + + if (tempDir.existsSync()) { + tempDir.deleteSync(recursive: true); + } + + final stdout = stdoutBuffer.toString(); + final timeline = stdout.split('\n'); + + expect(timeline.first, 'Timeline'); + expect( + timeline[1], + header, + ); + expect( + timeline[2], + 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', + ); + expect( + timeline[3].startsWith('Caller: at main. file:///'), + isTrue, + ); + expect( + timeline[4].startsWith( + 'Screenshot: file:///', + ), + isTrue, + ); + expect( + timeline[5].startsWith( + 'Timestamp:', + ), + isTrue, + ); + expect( + timeline[6], + separator, + ); + final prefix = isGlobalMode ? 'global' : 'local'; + final htmlLine = + timeline.firstWhere((line) => line.startsWith('View time line here:')); + expect( + htmlLine.endsWith( + 'timeline-$prefix-onerror-timeline-with-error-prints-timeline.html', + ), + isTrue, + ); + } + + static Future liveTimelineWithoutErrorPrintsHtml({ + bool isGlobalMode = false, + }) async { + final tempDir = Directory.systemTemp.createTempSync(); + final tempTestFile = File('${tempDir.path}/temp_test.dart'); + final testTitle = + '${isGlobalMode ? 'Global: ' : 'Local: '}Live timeline without error prints html'; + await tempTestFile.writeAsString( + testAsString( + title: testTitle, + timelineMode: TimelineMode.live, + isGlobalMode: isGlobalMode, + ), + ); + + final testProcess = + await TestProcess.start('flutter', ['test', tempTestFile.path]); + + final stdoutBuffer = StringBuffer(); + + bool write = false; + await for (final line in testProcess.stdoutStream()) { + if (line.isEmpty) continue; + + if (!write) { + if (line == header) { + write = true; + } + } + + if (write) { + stdoutBuffer.writeln(line); + } + } + + // Error does not happen + await testProcess.shouldExit(0); + + if (tempDir.existsSync()) { + tempDir.deleteSync(recursive: true); + } + + final stdout = stdoutBuffer.toString(); + final timeline = stdout.split('\n'); + // Does not start with 'Timeline', this only happens on error + expect(timeline.first, header); + expect( + timeline.second, + 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', + ); + expect( + timeline[2].startsWith('Caller: at'), + isTrue, + ); + expect( + timeline[3].startsWith( + 'Screenshot: file:///', + ), + isTrue, + ); + expect( + timeline[4].startsWith( + 'Timestamp:', + ), + isTrue, + ); + expect( + timeline[5], + separator, + ); + final htmlLine = + timeline.firstWhere((line) => line.startsWith('View time line here:')); + final prefix = isGlobalMode ? 'global' : 'local'; + expect( + htmlLine.endsWith( + 'timeline-$prefix-live-timeline-without-error-prints-html.html', + ), + isTrue, + ); + } + static Future offTimelineTest({ required WidgetTester tester, bool isGlobalMode = false, @@ -164,4 +341,43 @@ class TimelineTestHelpers { eventCount, ); } + + static String testAsString({ + required String title, + required TimelineMode timelineMode, + bool shouldFail = false, + bool isGlobalMode = false, + }) { + final globalInitiator = + isGlobalMode ? '${globalTimelineInitiator(timelineMode)};' : ''; + + final localInitiator = + isGlobalMode ? '' : '${localTimelineInitiator(timelineMode)};'; + + final widgetPart = File('test/timeline/tap/timeline_tap_test_widget.dart') + .readAsStringSync(); + + return ''' +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; +import 'package:spot/src/timeline/timeline.dart';\n +$widgetPart\n +void main() async { + $globalInitiator + final addButtonSelector = spotIcon(Icons.add); + final subtractButtonSelector = spotIcon(Icons.remove); + testWidgets("$title", (WidgetTester tester) async { + $localInitiator + await tester.pumpWidget(const TimelineTestWidget()); + addButtonSelector.existsOnce(); + spotText('Counter: 3').existsOnce(); + await act.tap(addButtonSelector); + spotText('Counter: 4').existsOnce(); + await act.tap(subtractButtonSelector); + spotText('Counter: 3').existsOnce(); + ${shouldFail ? 'spotText("Counter: 99").existsOnce();' : ''} + }); +} +'''; + } } From 91705f50be6a20053f3c3d450af9250c1cce27eb Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 26 Jun 2024 11:52:18 +0200 Subject: [PATCH 094/119] Add test "Global: Live timeline - Live timeline - with error, no duplicates, prints HTML" --- .../tap/global/live_timeline_tap_test.dart | 7 ++ .../timeline/tap/local/timeline_tap_test.dart | 109 +----------------- test/util/timeline_test_helpers.dart | 82 +++++++++++++ 3 files changed, 90 insertions(+), 108 deletions(-) diff --git a/test/timeline/tap/global/live_timeline_tap_test.dart b/test/timeline/tap/global/live_timeline_tap_test.dart index a0f442c4..503a582d 100644 --- a/test/timeline/tap/global/live_timeline_tap_test.dart +++ b/test/timeline/tap/global/live_timeline_tap_test.dart @@ -10,4 +10,11 @@ void main() { isGlobalMode: true, ); }); + test( + 'Global: Live timeline - Live timeline - with error, no duplicates, prints HTML', + () async { + await TimelineTestHelpers.liveTimelineWithErrorNoDuplicatesPrintsHtml( + isGlobalMode: true, + ); + }); } diff --git a/test/timeline/tap/local/timeline_tap_test.dart b/test/timeline/tap/local/timeline_tap_test.dart index 879f9f6f..3695ecc0 100644 --- a/test/timeline/tap/local/timeline_tap_test.dart +++ b/test/timeline/tap/local/timeline_tap_test.dart @@ -1,10 +1,6 @@ -import 'dart:io'; - -import 'package:dartx/dartx.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; -import 'package:test_process/test_process.dart'; import '../../../util/timeline_test_helpers.dart'; import '../timeline_tap_test_widget.dart'; @@ -13,37 +9,6 @@ final _subtractButtonSelector = spotIcon(Icons.remove); final _clearButtonSelector = spotIcon(Icons.clear); const _header = '==================== Timeline Event ===================='; -const _separator = '========================================================'; - -String _testAsString({ - required String title, - required TimelineMode timelineMode, - bool shouldFail = false, -}) { - final widgetPart = File('test/timeline/tap/timeline_tap_test_widget.dart') - .readAsStringSync(); - return ''' -import 'package:flutter_test/flutter_test.dart'; -import 'package:spot/spot.dart'; -import 'package:spot/src/timeline/timeline.dart';\n -$widgetPart\n -void main() async { - final addButtonSelector = spotIcon(Icons.add); - final subtractButtonSelector = spotIcon(Icons.remove); - testWidgets("$title", (WidgetTester tester) async { - ${TimelineTestHelpers.localTimelineInitiator(timelineMode)}; - await tester.pumpWidget(const TimelineTestWidget()); - addButtonSelector.existsOnce(); - spotText('Counter: 3').existsOnce(); - await act.tap(addButtonSelector); - spotText('Counter: 4').existsOnce(); - await act.tap(subtractButtonSelector); - spotText('Counter: 3').existsOnce(); - ${shouldFail ? 'spotText("Counter: 99").existsOnce();' : ''} - }); -} -'''; -} void main() { group('Initial Values', () { @@ -110,79 +75,7 @@ void main() { await TimelineTestHelpers.liveTimelineWithoutErrorPrintsHtml(); }); test('Live timeline - with error, no duplicates, prints HTML', () async { - final tempDir = Directory.systemTemp.createTempSync(); - final tempTestFile = File('${tempDir.path}/temp_test.dart'); - await tempTestFile.writeAsString( - _testAsString( - title: 'Live timeline - with error, no duplicates, prints HTML', - timelineMode: TimelineMode.live, - shouldFail: true, - ), - ); - - final testProcess = - await TestProcess.start('flutter', ['test', tempTestFile.path]); - - final stdoutBuffer = StringBuffer(); - - bool write = false; - await for (final line in testProcess.stdoutStream()) { - if (line.isEmpty) continue; - - if (!write) { - if (line == _header) { - write = true; - } - } - - if (write) { - stdoutBuffer.writeln(line); - } - } - - // Error does not happen - await testProcess.shouldExit(1); - - if (tempDir.existsSync()) { - tempDir.deleteSync(recursive: true); - } - - final stdout = stdoutBuffer.toString(); - final timeline = stdout.split('\n'); - // Does not start with 'Timeline', this only happens on error - expect(timeline.first, _header); - expect( - timeline.second, - 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', - ); - expect( - timeline[2].startsWith('Caller: at main. file:///'), - isTrue, - ); - expect( - timeline[3].startsWith( - 'Screenshot: file:///', - ), - isTrue, - ); - expect( - timeline[4].startsWith( - 'Timestamp:', - ), - isTrue, - ); - expect( - timeline[5], - _separator, - ); - final htmlLine = timeline - .firstWhere((line) => line.startsWith('View time line here:')); - expect( - htmlLine.endsWith( - 'live-timeline-with-error-no-duplicates-prints-html.html', - ), - isTrue, - ); + await TimelineTestHelpers.liveTimelineWithErrorNoDuplicatesPrintsHtml(); }); }); } diff --git a/test/util/timeline_test_helpers.dart b/test/util/timeline_test_helpers.dart index d6abd47d..debc7ae7 100644 --- a/test/util/timeline_test_helpers.dart +++ b/test/util/timeline_test_helpers.dart @@ -252,6 +252,88 @@ class TimelineTestHelpers { ); } + static Future liveTimelineWithErrorNoDuplicatesPrintsHtml({ + bool isGlobalMode = false, + }) async { + final tempDir = Directory.systemTemp.createTempSync(); + final tempTestFile = File('${tempDir.path}/temp_test.dart'); + final testTitle = + '${isGlobalMode ? 'Global: ' : 'Local: '}Live timeline - with error, no duplicates, prints HTML'; + await tempTestFile.writeAsString( + testAsString( + title: testTitle, + timelineMode: TimelineMode.live, + shouldFail: true, + isGlobalMode: isGlobalMode, + ), + ); + + final testProcess = + await TestProcess.start('flutter', ['test', tempTestFile.path]); + + final stdoutBuffer = StringBuffer(); + + bool write = false; + await for (final line in testProcess.stdoutStream()) { + if (line.isEmpty) continue; + + if (!write) { + if (line == header) { + write = true; + } + } + + if (write) { + stdoutBuffer.writeln(line); + } + } + + // Error does not happen + await testProcess.shouldExit(1); + + if (tempDir.existsSync()) { + tempDir.deleteSync(recursive: true); + } + + final stdout = stdoutBuffer.toString(); + final timeline = stdout.split('\n'); + // Does not start with 'Timeline', this only happens on error + expect(timeline.first, header); + expect( + timeline.second, + 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', + ); + expect( + timeline[2].startsWith('Caller: at main. file:///'), + isTrue, + ); + expect( + timeline[3].startsWith( + 'Screenshot: file:///', + ), + isTrue, + ); + expect( + timeline[4].startsWith( + 'Timestamp:', + ), + isTrue, + ); + expect( + timeline[5], + separator, + ); + final prefix = isGlobalMode ? 'global' : 'local'; + final htmlLine = + timeline.firstWhere((line) => line.startsWith('View time line here:')); + expect( + htmlLine.endsWith( + 'timeline-$prefix-live-timeline-with-error-no-duplicates-prints-html.html', + ), + isTrue, + ); + } + static Future offTimelineTest({ required WidgetTester tester, bool isGlobalMode = false, From d38d2833c51f311c83683e5671558b7fb6236f0e Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 26 Jun 2024 12:43:27 +0200 Subject: [PATCH 095/119] Add test "Global: Turn live timeline off during test" --- lib/src/timeline/timeline.dart | 4 + .../tap/global/live_timeline_tap_test.dart | 6 ++ .../timeline/tap/local/timeline_tap_test.dart | 93 +++---------------- test/util/timeline_test_helpers.dart | 56 ++++++++++- 4 files changed, 75 insertions(+), 84 deletions(-) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index f689e23e..fb0b8e2e 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -44,6 +44,10 @@ set globalTimelineMode(TimelineMode value) { print(value.message); } _globalTimelineMode = value; + final test = Invoker.current?.liveTest; + if (test != null) { + timeline.mode = value; + } } /// ... diff --git a/test/timeline/tap/global/live_timeline_tap_test.dart b/test/timeline/tap/global/live_timeline_tap_test.dart index 503a582d..1b239e5f 100644 --- a/test/timeline/tap/global/live_timeline_tap_test.dart +++ b/test/timeline/tap/global/live_timeline_tap_test.dart @@ -17,4 +17,10 @@ void main() { isGlobalMode: true, ); }); + testWidgets('Global: Turn live timeline off during test', (tester) async { + await TimelineTestHelpers.liveTimelineTurnOffDuringTest( + isGlobalMode: true, + tester: tester, + ); + }); } diff --git a/test/timeline/tap/local/timeline_tap_test.dart b/test/timeline/tap/local/timeline_tap_test.dart index 3695ecc0..9dfb8d18 100644 --- a/test/timeline/tap/local/timeline_tap_test.dart +++ b/test/timeline/tap/local/timeline_tap_test.dart @@ -1,111 +1,40 @@ -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import '../../../util/timeline_test_helpers.dart'; -import '../timeline_tap_test_widget.dart'; - -final _addButtonSelector = spotIcon(Icons.add); -final _subtractButtonSelector = spotIcon(Icons.remove); -final _clearButtonSelector = spotIcon(Icons.clear); - -const _header = '==================== Timeline Event ===================='; void main() { group('Initial Values', () { - test('global timeline', () { + test('Global Timeline', () { expect(globalTimelineMode, TimelineMode.record); }); - test('local timeline', () { + test('Local Timeline', () { expect(localTimelineMode, isNull); }); }); group('Override global timeline', () { - testWidgets('Start with Timeline Mode off', (tester) async { - TimelineTestHelpers.offTimelineTest(tester: tester); + testWidgets('Local: Start with Timeline Mode off', (tester) async { + await TimelineTestHelpers.offTimelineTest(tester: tester); }); - testWidgets('Turn timeline mode off during test', (tester) async { - final output = await TimelineTestHelpers.captureConsoleOutput(() async { - localTimelineMode = TimelineMode.live; - await tester.pumpWidget( - const TimelineTestWidget(), - ); - spotText('Counter: 3').existsOnce(); - _addButtonSelector.existsOnce(); - await act.tap(_addButtonSelector); - spotText('Counter: 4').existsOnce(); - await act.tap(_subtractButtonSelector); - spotText('Counter: 3').existsOnce(); - // Notify that the recording stopped - localTimelineMode = TimelineMode.off; - await act.tap(_clearButtonSelector); - spotText('Counter: 0').existsOnce(); - // Notify that the recording is already off - localTimelineMode = TimelineMode.off; - }); - expect(output, contains('馃敶 - Recording live timeline')); - expect( - output, - contains('Tap ${_addButtonSelector.toStringBreadcrumb()}'), - ); - expect( - output, - contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), - ); - // No further events were added to the timeline, including screenshots - expect( - output, - isNot(contains('Tap ${_clearButtonSelector.toStringBreadcrumb()}')), - ); - _testTimeLineContent(output: output, eventCount: 2); - expect(output, contains('鈴革笌 - Timeline recording is off')); - expect(output, contains('Timeline mode is already set to "off"')); + testWidgets('Local: Turn live timeline off during test', (tester) async { + await TimelineTestHelpers.liveTimelineTurnOffDuringTest(tester: tester); }); }); group('Print on teardown', () { - testWidgets('OnError timeline - without error', (tester) async { + testWidgets('Local: OnError timeline - without error', (tester) async { await TimelineTestHelpers.recordTimelineTestWithoutError(tester: tester); }); - test('OnError timeline - with error, prints timeline', () async { + test('Local: OnError timeline - with error, prints timeline', () async { await TimelineTestHelpers.recordTimelineTestWithError(); }); - test('Live timeline - without error, prints HTML', () async { + test('Local: Live timeline - without error, prints HTML', () async { await TimelineTestHelpers.liveTimelineWithoutErrorPrintsHtml(); }); - test('Live timeline - with error, no duplicates, prints HTML', () async { + test('Local: Live timeline - with error, no duplicates, prints HTML', + () async { await TimelineTestHelpers.liveTimelineWithErrorNoDuplicatesPrintsHtml(); }); }); } - -void _testTimeLineContent({ - required String output, - required int eventCount, -}) { - expect( - RegExp(_header).allMatches(output).length, - eventCount, - ); - expect( - RegExp('Event: Tap Icon Widget with icon:').allMatches(output).length, - eventCount, - ); - final callerParts = output.split('\n').where((line) { - return line.startsWith('Caller: at main') && line.contains('file://'); - }).toList(); - - expect( - callerParts.length, - eventCount, - ); - expect( - RegExp('Screenshot: file:').allMatches(output).length, - eventCount, - ); - expect( - RegExp('Timestamp: ').allMatches(output).length, - eventCount, - ); -} diff --git a/test/util/timeline_test_helpers.dart b/test/util/timeline_test_helpers.dart index debc7ae7..10a229e5 100644 --- a/test/util/timeline_test_helpers.dart +++ b/test/util/timeline_test_helpers.dart @@ -363,6 +363,59 @@ class TimelineTestHelpers { _testTimeLineContent(output: output, eventCount: 0); } + static Future liveTimelineTurnOffDuringTest({ + required WidgetTester tester, + bool isGlobalMode = false, + }) async { + print('isGlobalMode: $isGlobalMode'); + final output = await captureConsoleOutput(() async { + if (!isGlobalMode) { + localTimelineMode = TimelineMode.live; + } + await tester.pumpWidget( + const TimelineTestWidget(), + ); + spotText('Counter: 3').existsOnce(); + addButtonSelector.existsOnce(); + await act.tap(addButtonSelector); + spotText('Counter: 4').existsOnce(); + await act.tap(subtractButtonSelector); + spotText('Counter: 3').existsOnce(); + // Notify that the recording stopped + if (isGlobalMode) { + globalTimelineMode = TimelineMode.off; + } else { + localTimelineMode = TimelineMode.off; + } + await act.tap(clearButtonSelector); + spotText('Counter: 0').existsOnce(); + // Notify that the recording is already off + if (isGlobalMode) { + globalTimelineMode = TimelineMode.off; + } else { + localTimelineMode = TimelineMode.off; + } + }); + print('output: $output'); + expect(output, contains('馃敶 - Recording live timeline')); + expect( + output, + contains('Tap ${addButtonSelector.toStringBreadcrumb()}'), + ); + expect( + output, + contains('Tap ${subtractButtonSelector.toStringBreadcrumb()}'), + ); + // No further events were added to the timeline, including screenshots + expect( + output, + isNot(contains('Tap ${clearButtonSelector.toStringBreadcrumb()}')), + ); + _testTimeLineContent(output: output, eventCount: 2); + expect(output, contains('鈴革笌 - Timeline recording is off')); + expect(output, contains('Timeline mode is already set to "off"')); + } + static Future liveTimelineTest({ required WidgetTester tester, bool isGlobalMode = false, @@ -407,9 +460,8 @@ class TimelineTestHelpers { eventCount, ); final callerParts = output.split('\n').where((line) { - return line.startsWith('Caller: at main') && line.contains('file://'); + return line.startsWith('Caller: at') && line.contains('file://'); }).toList(); - expect( callerParts.length, eventCount, From 51f968740ec30a7f8987c97ea51e41f2d90e1418 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 26 Jun 2024 12:45:20 +0200 Subject: [PATCH 096/119] Rename tests --- ..._timeline_tap_test.dart => global_live_timeline_tap_test.dart} | 0 ...f_timeline_tap_test.dart => global_off_timeline_tap_test.dart} | 0 ...imeline_tap_test.dart => global_record_timeline_tap_test.dart} | 0 .../{timeline_tap_test.dart => local_timeline_tap_test.dart} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename test/timeline/tap/global/{live_timeline_tap_test.dart => global_live_timeline_tap_test.dart} (100%) rename test/timeline/tap/global/{off_timeline_tap_test.dart => global_off_timeline_tap_test.dart} (100%) rename test/timeline/tap/global/{record_timeline_tap_test.dart => global_record_timeline_tap_test.dart} (100%) rename test/timeline/tap/local/{timeline_tap_test.dart => local_timeline_tap_test.dart} (100%) diff --git a/test/timeline/tap/global/live_timeline_tap_test.dart b/test/timeline/tap/global/global_live_timeline_tap_test.dart similarity index 100% rename from test/timeline/tap/global/live_timeline_tap_test.dart rename to test/timeline/tap/global/global_live_timeline_tap_test.dart diff --git a/test/timeline/tap/global/off_timeline_tap_test.dart b/test/timeline/tap/global/global_off_timeline_tap_test.dart similarity index 100% rename from test/timeline/tap/global/off_timeline_tap_test.dart rename to test/timeline/tap/global/global_off_timeline_tap_test.dart diff --git a/test/timeline/tap/global/record_timeline_tap_test.dart b/test/timeline/tap/global/global_record_timeline_tap_test.dart similarity index 100% rename from test/timeline/tap/global/record_timeline_tap_test.dart rename to test/timeline/tap/global/global_record_timeline_tap_test.dart diff --git a/test/timeline/tap/local/timeline_tap_test.dart b/test/timeline/tap/local/local_timeline_tap_test.dart similarity index 100% rename from test/timeline/tap/local/timeline_tap_test.dart rename to test/timeline/tap/local/local_timeline_tap_test.dart From ee9f640d77d285d00f5ede6d8854a4d395761140 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 26 Jun 2024 13:15:54 +0200 Subject: [PATCH 097/119] Move capture console output into separate file --- test/timeline/drag/timeline_drag_test.dart | 9 +- test/util/capture_console_output.dart | 18 +++ test/util/timeline_test_helpers.dart | 137 +++++++++------------ 3 files changed, 79 insertions(+), 85 deletions(-) create mode 100644 test/util/capture_console_output.dart diff --git a/test/timeline/drag/timeline_drag_test.dart b/test/timeline/drag/timeline_drag_test.dart index e0b5a918..0d670997 100644 --- a/test/timeline/drag/timeline_drag_test.dart +++ b/test/timeline/drag/timeline_drag_test.dart @@ -4,6 +4,7 @@ import 'package:dartx/dartx.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import 'package:test_process/test_process.dart'; +import '../../util/capture_console_output.dart'; import '../../util/timeline_test_helpers.dart'; import 'drag_until_visible_test_widget.dart'; @@ -67,7 +68,7 @@ void main() { group('Drag Timeline Test', () { group('Without error', () { testWidgets('Drag Until Visible - Live timeline', (tester) async { - final output = await TimelineTestHelpers.captureConsoleOutput(() async { + final output = await captureConsoleOutput(() async { localTimelineMode = TimelineMode.live; await _testBody(tester); // Notify that the timeline of this type is already recording. @@ -82,7 +83,7 @@ void main() { expect(output, contains('馃敶 - Already recording live timeline')); }); testWidgets('Start with Timeline Mode off', (tester) async { - final output = await TimelineTestHelpers.captureConsoleOutput(() async { + final output = await captureConsoleOutput(() async { localTimelineMode = TimelineMode.off; await _testBody(tester); }); @@ -93,7 +94,7 @@ void main() { expect(splitted.first, expectedOutput); }); testWidgets('Turn timeline mode off during test', (tester) async { - final output = await TimelineTestHelpers.captureConsoleOutput(() async { + final output = await captureConsoleOutput(() async { localTimelineMode = TimelineMode.record; await _testBody(tester); // Notify that the recording is off @@ -112,7 +113,7 @@ void main() { expect(output, contains('鈴革笌 - Timeline recording is off')); }); testWidgets('act.drag: OnError timeline - without error', (tester) async { - final output = await TimelineTestHelpers.captureConsoleOutput(() async { + final output = await captureConsoleOutput(() async { localTimelineMode = TimelineMode.record; await _testBody(tester); localTimelineMode = TimelineMode.record; diff --git a/test/util/capture_console_output.dart b/test/util/capture_console_output.dart new file mode 100644 index 00000000..c34ee4d0 --- /dev/null +++ b/test/util/capture_console_output.dart @@ -0,0 +1,18 @@ +import 'dart:async'; + +Future captureConsoleOutput( + Future Function() testFunction, +) async { + final StringBuffer buffer = StringBuffer(); + final ZoneSpecification spec = ZoneSpecification( + print: (self, parent, zone, line) { + buffer.writeln(line); + }, + ); + + await Zone.current.fork(specification: spec).run(() async { + await testFunction(); + }); + + return buffer.toString(); +} diff --git a/test/util/timeline_test_helpers.dart b/test/util/timeline_test_helpers.dart index 10a229e5..cd865d5a 100644 --- a/test/util/timeline_test_helpers.dart +++ b/test/util/timeline_test_helpers.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:io'; import 'package:dartx/dartx.dart'; @@ -8,70 +7,48 @@ import 'package:spot/spot.dart'; import 'package:test_process/test_process.dart'; import '../timeline/tap/timeline_tap_test_widget.dart'; +import 'capture_console_output.dart'; class TimelineTestHelpers { - static final WidgetSelector addButtonSelector = spotIcon(Icons.add); - static final subtractButtonSelector = spotIcon(Icons.remove); - static final clearButtonSelector = spotIcon(Icons.clear); + static final WidgetSelector _addButtonSelector = spotIcon(Icons.add); + static final _subtractButtonSelector = spotIcon(Icons.remove); + static final _clearButtonSelector = spotIcon(Icons.clear); - static const header = + static const _header = '==================== Timeline Event ===================='; - static const separator = + static const _separator = '========================================================'; - static Future captureConsoleOutput( - Future Function() testFunction, - ) async { - final StringBuffer buffer = StringBuffer(); - final ZoneSpecification spec = ZoneSpecification( - print: (self, parent, zone, line) { - buffer.writeln(line); - }, - ); - - await Zone.current.fork(specification: spec).run(() async { - await testFunction(); - }); - - return buffer.toString(); - } - static String localTimelineInitiator(TimelineMode timelineMode) { - switch (timelineMode) { - case TimelineMode.live: - return 'localTimelineMode = TimelineMode.live;'; - case TimelineMode.record: - return 'localTimelineMode = TimelineMode.record;'; - case TimelineMode.off: - return 'localTimelineMode = TimelineMode.off;'; - } + return switch (timelineMode) { + TimelineMode.live => 'localTimelineMode = TimelineMode.live;', + TimelineMode.record => 'localTimelineMode = TimelineMode.record;', + TimelineMode.off => 'localTimelineMode = TimelineMode.off;', + }; } - static String globalTimelineInitiator(TimelineMode timelineMode) { - switch (timelineMode) { - case TimelineMode.live: - return 'globalTimelineMode = TimelineMode.live;'; - case TimelineMode.record: - return 'globalTimelineMode = TimelineMode.record;'; - case TimelineMode.off: - return 'globalTimelineMode = TimelineMode.off;'; - } + static String _globalTimelineInitiator(TimelineMode timelineMode) { + return switch (timelineMode) { + TimelineMode.live => 'globalTimelineMode = TimelineMode.live;', + TimelineMode.record => 'globalTimelineMode = TimelineMode.record;', + TimelineMode.off => 'globalTimelineMode = TimelineMode.off;', + }; } static Future recordTimelineTestWithoutError({ required WidgetTester tester, bool isGlobalMode = false, }) async { - final output = await TimelineTestHelpers.captureConsoleOutput(() async { + final output = await captureConsoleOutput(() async { if (!isGlobalMode) { localTimelineMode = TimelineMode.record; } await tester.pumpWidget(const TimelineTestWidget()); - addButtonSelector.existsOnce(); + _addButtonSelector.existsOnce(); spotText('Counter: 3').existsOnce(); - await act.tap(addButtonSelector); + await act.tap(_addButtonSelector); spotText('Counter: 4').existsOnce(); - await act.tap(subtractButtonSelector); + await act.tap(_subtractButtonSelector); spotText('Counter: 3').existsOnce(); // Notify that the timeline of this type is already recording. localTimelineMode = TimelineMode.record; @@ -79,11 +56,11 @@ class TimelineTestHelpers { expect(output, contains('馃敶 - Recording error output timeline')); expect( output, - isNot(contains('Tap ${addButtonSelector.toStringBreadcrumb()}')), + isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), ); expect( output, - isNot(contains('Tap ${subtractButtonSelector.toStringBreadcrumb()}')), + isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), ); expect(output, contains('Timeline mode is already set to "record"')); _testTimeLineContent(output: output, eventCount: 0); @@ -97,7 +74,7 @@ class TimelineTestHelpers { final testTitle = '${isGlobalMode ? 'Global: ' : 'Local: '}OnError timeline - with error, prints timeline'; await tempTestFile.writeAsString( - testAsString( + _tapTestAsString( title: testTitle, timelineMode: TimelineMode.record, shouldFail: true, @@ -134,7 +111,7 @@ class TimelineTestHelpers { expect(timeline.first, 'Timeline'); expect( timeline[1], - header, + _header, ); expect( timeline[2], @@ -158,7 +135,7 @@ class TimelineTestHelpers { ); expect( timeline[6], - separator, + _separator, ); final prefix = isGlobalMode ? 'global' : 'local'; final htmlLine = @@ -179,7 +156,7 @@ class TimelineTestHelpers { final testTitle = '${isGlobalMode ? 'Global: ' : 'Local: '}Live timeline without error prints html'; await tempTestFile.writeAsString( - testAsString( + _tapTestAsString( title: testTitle, timelineMode: TimelineMode.live, isGlobalMode: isGlobalMode, @@ -196,7 +173,7 @@ class TimelineTestHelpers { if (line.isEmpty) continue; if (!write) { - if (line == header) { + if (line == _header) { write = true; } } @@ -216,7 +193,7 @@ class TimelineTestHelpers { final stdout = stdoutBuffer.toString(); final timeline = stdout.split('\n'); // Does not start with 'Timeline', this only happens on error - expect(timeline.first, header); + expect(timeline.first, _header); expect( timeline.second, 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', @@ -239,7 +216,7 @@ class TimelineTestHelpers { ); expect( timeline[5], - separator, + _separator, ); final htmlLine = timeline.firstWhere((line) => line.startsWith('View time line here:')); @@ -260,7 +237,7 @@ class TimelineTestHelpers { final testTitle = '${isGlobalMode ? 'Global: ' : 'Local: '}Live timeline - with error, no duplicates, prints HTML'; await tempTestFile.writeAsString( - testAsString( + _tapTestAsString( title: testTitle, timelineMode: TimelineMode.live, shouldFail: true, @@ -278,7 +255,7 @@ class TimelineTestHelpers { if (line.isEmpty) continue; if (!write) { - if (line == header) { + if (line == _header) { write = true; } } @@ -298,7 +275,7 @@ class TimelineTestHelpers { final stdout = stdoutBuffer.toString(); final timeline = stdout.split('\n'); // Does not start with 'Timeline', this only happens on error - expect(timeline.first, header); + expect(timeline.first, _header); expect( timeline.second, 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', @@ -321,7 +298,7 @@ class TimelineTestHelpers { ); expect( timeline[5], - separator, + _separator, ); final prefix = isGlobalMode ? 'global' : 'local'; final htmlLine = @@ -338,27 +315,27 @@ class TimelineTestHelpers { required WidgetTester tester, bool isGlobalMode = false, }) async { - final output = await TimelineTestHelpers.captureConsoleOutput(() async { + final output = await captureConsoleOutput(() async { if (!isGlobalMode) { localTimelineMode = TimelineMode.off; } await tester.pumpWidget(const TimelineTestWidget()); - addButtonSelector.existsOnce(); + _addButtonSelector.existsOnce(); spotText('Counter: 3').existsOnce(); - await act.tap(addButtonSelector); + await act.tap(_addButtonSelector); spotText('Counter: 4').existsOnce(); - await act.tap(subtractButtonSelector); + await act.tap(_subtractButtonSelector); spotText('Counter: 3').existsOnce(); }); expect(output, contains('鈴革笌 - Timeline recording is off')); expect( output, - isNot(contains('Tap ${addButtonSelector.toStringBreadcrumb()}')), + isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), ); expect( output, - isNot(contains('Tap ${subtractButtonSelector.toStringBreadcrumb()}')), + isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), ); _testTimeLineContent(output: output, eventCount: 0); } @@ -367,7 +344,6 @@ class TimelineTestHelpers { required WidgetTester tester, bool isGlobalMode = false, }) async { - print('isGlobalMode: $isGlobalMode'); final output = await captureConsoleOutput(() async { if (!isGlobalMode) { localTimelineMode = TimelineMode.live; @@ -376,10 +352,10 @@ class TimelineTestHelpers { const TimelineTestWidget(), ); spotText('Counter: 3').existsOnce(); - addButtonSelector.existsOnce(); - await act.tap(addButtonSelector); + _addButtonSelector.existsOnce(); + await act.tap(_addButtonSelector); spotText('Counter: 4').existsOnce(); - await act.tap(subtractButtonSelector); + await act.tap(_subtractButtonSelector); spotText('Counter: 3').existsOnce(); // Notify that the recording stopped if (isGlobalMode) { @@ -387,7 +363,7 @@ class TimelineTestHelpers { } else { localTimelineMode = TimelineMode.off; } - await act.tap(clearButtonSelector); + await act.tap(_clearButtonSelector); spotText('Counter: 0').existsOnce(); // Notify that the recording is already off if (isGlobalMode) { @@ -396,20 +372,19 @@ class TimelineTestHelpers { localTimelineMode = TimelineMode.off; } }); - print('output: $output'); expect(output, contains('馃敶 - Recording live timeline')); expect( output, - contains('Tap ${addButtonSelector.toStringBreadcrumb()}'), + contains('Tap ${_addButtonSelector.toStringBreadcrumb()}'), ); expect( output, - contains('Tap ${subtractButtonSelector.toStringBreadcrumb()}'), + contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), ); // No further events were added to the timeline, including screenshots expect( output, - isNot(contains('Tap ${clearButtonSelector.toStringBreadcrumb()}')), + isNot(contains('Tap ${_clearButtonSelector.toStringBreadcrumb()}')), ); _testTimeLineContent(output: output, eventCount: 2); expect(output, contains('鈴革笌 - Timeline recording is off')); @@ -420,16 +395,16 @@ class TimelineTestHelpers { required WidgetTester tester, bool isGlobalMode = false, }) async { - final output = await TimelineTestHelpers.captureConsoleOutput(() async { + final output = await captureConsoleOutput(() async { if (!isGlobalMode) { localTimelineMode = TimelineMode.live; } await tester.pumpWidget(const TimelineTestWidget()); - addButtonSelector.existsOnce(); + _addButtonSelector.existsOnce(); spotText('Counter: 3').existsOnce(); - await act.tap(addButtonSelector); + await act.tap(_addButtonSelector); spotText('Counter: 4').existsOnce(); - await act.tap(subtractButtonSelector); + await act.tap(_subtractButtonSelector); spotText('Counter: 3').existsOnce(); // Notify that the timeline mode is already set to live localTimelineMode = TimelineMode.live; @@ -437,11 +412,11 @@ class TimelineTestHelpers { expect(output, contains('馃敶 - Recording live timeline')); expect( output, - contains('Event: Tap ${addButtonSelector.toStringBreadcrumb()}'), + contains('Event: Tap ${_addButtonSelector.toStringBreadcrumb()}'), ); expect( output, - contains('Event: Tap ${subtractButtonSelector.toStringBreadcrumb()}'), + contains('Event: Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), ); expect(output, contains('Timeline mode is already set to "live"')); _testTimeLineContent(output: output, eventCount: 2); @@ -452,7 +427,7 @@ class TimelineTestHelpers { required int eventCount, }) { expect( - RegExp(header).allMatches(output).length, + RegExp(_header).allMatches(output).length, eventCount, ); expect( @@ -476,14 +451,14 @@ class TimelineTestHelpers { ); } - static String testAsString({ + static String _tapTestAsString({ required String title, required TimelineMode timelineMode, bool shouldFail = false, bool isGlobalMode = false, }) { final globalInitiator = - isGlobalMode ? '${globalTimelineInitiator(timelineMode)};' : ''; + isGlobalMode ? '${_globalTimelineInitiator(timelineMode)};' : ''; final localInitiator = isGlobalMode ? '' : '${localTimelineInitiator(timelineMode)};'; From f9ff5dfbcb09689ae96c185d055906592704a762 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 26 Jun 2024 13:41:37 +0200 Subject: [PATCH 098/119] Add test "Global: record, without error" and refactor --- .../global/global_live_timeline_tap_test.dart | 20 ++++++++----- .../global/global_off_timeline_tap_test.dart | 4 +-- .../global_record_timeline_tap_test.dart | 8 ++--- .../tap/local/local_timeline_tap_test.dart | 28 +++++++++-------- test/util/timeline_test_helpers.dart | 30 ++++++++----------- 5 files changed, 46 insertions(+), 44 deletions(-) diff --git a/test/timeline/tap/global/global_live_timeline_tap_test.dart b/test/timeline/tap/global/global_live_timeline_tap_test.dart index 1b239e5f..6f909d11 100644 --- a/test/timeline/tap/global/global_live_timeline_tap_test.dart +++ b/test/timeline/tap/global/global_live_timeline_tap_test.dart @@ -5,20 +5,24 @@ import '../../../util/timeline_test_helpers.dart'; void main() { globalTimelineMode = TimelineMode.live; - test('Global: Live timeline - without error, prints HTML', () async { - await TimelineTestHelpers.liveTimelineWithoutErrorPrintsHtml( + testWidgets('Global: record, without error', (tester) async { + await TimelineTestHelpers.liveWithoutError( + tester: tester, + isGlobalMode: true, + ); + }); + test('Global: live - without error, prints HTML', () async { + await TimelineTestHelpers.liveWithoutErrorPrintsHtml( isGlobalMode: true, ); }); - test( - 'Global: Live timeline - Live timeline - with error, no duplicates, prints HTML', - () async { - await TimelineTestHelpers.liveTimelineWithErrorNoDuplicatesPrintsHtml( + test('Global: live - with error, no duplicates, prints HTML', () async { + await TimelineTestHelpers.liveWithErrorNoDuplicatesPrintsHtml( isGlobalMode: true, ); }); - testWidgets('Global: Turn live timeline off during test', (tester) async { - await TimelineTestHelpers.liveTimelineTurnOffDuringTest( + testWidgets('Global: live, turn off during test', (tester) async { + await TimelineTestHelpers.liveTurnOffDuringTest( isGlobalMode: true, tester: tester, ); diff --git a/test/timeline/tap/global/global_off_timeline_tap_test.dart b/test/timeline/tap/global/global_off_timeline_tap_test.dart index 1c89b267..70c39e82 100644 --- a/test/timeline/tap/global/global_off_timeline_tap_test.dart +++ b/test/timeline/tap/global/global_off_timeline_tap_test.dart @@ -5,8 +5,8 @@ import '../../../util/timeline_test_helpers.dart'; void main() { globalTimelineMode = TimelineMode.off; - testWidgets('Global timeline - Timeline off', (tester) async { - await TimelineTestHelpers.offTimelineTest( + testWidgets('Global: off, without error', (tester) async { + await TimelineTestHelpers.offWithoutError( tester: tester, isGlobalMode: true, ); diff --git a/test/timeline/tap/global/global_record_timeline_tap_test.dart b/test/timeline/tap/global/global_record_timeline_tap_test.dart index 24aa26ac..c62102e2 100644 --- a/test/timeline/tap/global/global_record_timeline_tap_test.dart +++ b/test/timeline/tap/global/global_record_timeline_tap_test.dart @@ -5,13 +5,13 @@ import '../../../util/timeline_test_helpers.dart'; void main() { globalTimelineMode = TimelineMode.record; - testWidgets('Global timeline - record, without error', (tester) async { - await TimelineTestHelpers.recordTimelineTestWithoutError( + testWidgets('Global: record, without error', (tester) async { + await TimelineTestHelpers.recordWithoutError( tester: tester, isGlobalMode: true, ); }); - test('Global timeline - record, with error', () async { - await TimelineTestHelpers.recordTimelineTestWithError(isGlobalMode: true); + test('Global: record, with error', () async { + await TimelineTestHelpers.recordWithError(isGlobalMode: true); }); } diff --git a/test/timeline/tap/local/local_timeline_tap_test.dart b/test/timeline/tap/local/local_timeline_tap_test.dart index 9dfb8d18..6eebe123 100644 --- a/test/timeline/tap/local/local_timeline_tap_test.dart +++ b/test/timeline/tap/local/local_timeline_tap_test.dart @@ -13,28 +13,30 @@ void main() { }); group('Override global timeline', () { - testWidgets('Local: Start with Timeline Mode off', (tester) async { - await TimelineTestHelpers.offTimelineTest(tester: tester); + testWidgets('Local: live, without error', (tester) async { + await TimelineTestHelpers.liveWithoutError(tester: tester); }); - testWidgets('Local: Turn live timeline off during test', (tester) async { - await TimelineTestHelpers.liveTimelineTurnOffDuringTest(tester: tester); + testWidgets('Local: off, without error', (tester) async { + await TimelineTestHelpers.offWithoutError(tester: tester); + }); + testWidgets('Local: live, turn off during test', (tester) async { + await TimelineTestHelpers.liveTurnOffDuringTest(tester: tester); }); }); group('Print on teardown', () { - testWidgets('Local: OnError timeline - without error', (tester) async { - await TimelineTestHelpers.recordTimelineTestWithoutError(tester: tester); + testWidgets('Local: record, without error', (tester) async { + await TimelineTestHelpers.recordWithoutError(tester: tester); }); - test('Local: OnError timeline - with error, prints timeline', () async { - await TimelineTestHelpers.recordTimelineTestWithError(); + test('Local: record, with error', () async { + await TimelineTestHelpers.recordWithError(); }); - test('Local: Live timeline - without error, prints HTML', () async { - await TimelineTestHelpers.liveTimelineWithoutErrorPrintsHtml(); + test('Local: live - without error, prints HTML', () async { + await TimelineTestHelpers.liveWithoutErrorPrintsHtml(); }); - test('Local: Live timeline - with error, no duplicates, prints HTML', - () async { - await TimelineTestHelpers.liveTimelineWithErrorNoDuplicatesPrintsHtml(); + test('Local: live - with error, no duplicates, prints HTML', () async { + await TimelineTestHelpers.liveWithErrorNoDuplicatesPrintsHtml(); }); }); } diff --git a/test/util/timeline_test_helpers.dart b/test/util/timeline_test_helpers.dart index cd865d5a..7494699d 100644 --- a/test/util/timeline_test_helpers.dart +++ b/test/util/timeline_test_helpers.dart @@ -35,7 +35,7 @@ class TimelineTestHelpers { }; } - static Future recordTimelineTestWithoutError({ + static Future recordWithoutError({ required WidgetTester tester, bool isGlobalMode = false, }) async { @@ -66,16 +66,14 @@ class TimelineTestHelpers { _testTimeLineContent(output: output, eventCount: 0); } - static Future recordTimelineTestWithError({ + static Future recordWithError({ bool isGlobalMode = false, }) async { final tempDir = Directory.systemTemp.createTempSync(); final tempTestFile = File('${tempDir.path}/temp_test.dart'); - final testTitle = - '${isGlobalMode ? 'Global: ' : 'Local: '}OnError timeline - with error, prints timeline'; await tempTestFile.writeAsString( _tapTestAsString( - title: testTitle, + title: 'OnError timeline - with error, prints timeline', timelineMode: TimelineMode.record, shouldFail: true, isGlobalMode: isGlobalMode, @@ -148,16 +146,14 @@ class TimelineTestHelpers { ); } - static Future liveTimelineWithoutErrorPrintsHtml({ + static Future liveWithoutErrorPrintsHtml({ bool isGlobalMode = false, }) async { final tempDir = Directory.systemTemp.createTempSync(); final tempTestFile = File('${tempDir.path}/temp_test.dart'); - final testTitle = - '${isGlobalMode ? 'Global: ' : 'Local: '}Live timeline without error prints html'; await tempTestFile.writeAsString( _tapTestAsString( - title: testTitle, + title: 'Live timeline without error prints html', timelineMode: TimelineMode.live, isGlobalMode: isGlobalMode, ), @@ -229,16 +225,14 @@ class TimelineTestHelpers { ); } - static Future liveTimelineWithErrorNoDuplicatesPrintsHtml({ + static Future liveWithErrorNoDuplicatesPrintsHtml({ bool isGlobalMode = false, }) async { final tempDir = Directory.systemTemp.createTempSync(); final tempTestFile = File('${tempDir.path}/temp_test.dart'); - final testTitle = - '${isGlobalMode ? 'Global: ' : 'Local: '}Live timeline - with error, no duplicates, prints HTML'; await tempTestFile.writeAsString( _tapTestAsString( - title: testTitle, + title: 'Live timeline - with error, no duplicates, prints HTML', timelineMode: TimelineMode.live, shouldFail: true, isGlobalMode: isGlobalMode, @@ -311,7 +305,7 @@ class TimelineTestHelpers { ); } - static Future offTimelineTest({ + static Future offWithoutError({ required WidgetTester tester, bool isGlobalMode = false, }) async { @@ -340,7 +334,7 @@ class TimelineTestHelpers { _testTimeLineContent(output: output, eventCount: 0); } - static Future liveTimelineTurnOffDuringTest({ + static Future liveTurnOffDuringTest({ required WidgetTester tester, bool isGlobalMode = false, }) async { @@ -391,7 +385,7 @@ class TimelineTestHelpers { expect(output, contains('Timeline mode is already set to "off"')); } - static Future liveTimelineTest({ + static Future liveWithoutError({ required WidgetTester tester, bool isGlobalMode = false, }) async { @@ -457,6 +451,8 @@ class TimelineTestHelpers { bool shouldFail = false, bool isGlobalMode = false, }) { + final testTitle = '${isGlobalMode ? 'Global: ' : 'Local: '}$title'; + final globalInitiator = isGlobalMode ? '${_globalTimelineInitiator(timelineMode)};' : ''; @@ -475,7 +471,7 @@ void main() async { $globalInitiator final addButtonSelector = spotIcon(Icons.add); final subtractButtonSelector = spotIcon(Icons.remove); - testWidgets("$title", (WidgetTester tester) async { + testWidgets("$testTitle", (WidgetTester tester) async { $localInitiator await tester.pumpWidget(const TimelineTestWidget()); addButtonSelector.existsOnce(); From bd407a4e906db6769e98dadd0cd7e4002fd5346a Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 26 Jun 2024 15:55:53 +0200 Subject: [PATCH 099/119] Rm timeline test helpers --- test/timeline/drag/timeline_drag_test.dart | 5 +- .../global/global_live_timeline_tap_test.dart | 11 +- .../global/global_off_timeline_tap_test.dart | 5 +- .../global_record_timeline_tap_test.dart | 7 +- .../tap/local/local_timeline_tap_test.dart | 26 +- .../tap/timeline_tap_test_bodies.dart | 396 ++++++++++++++ test/timeline/timeline_test_shared.dart | 23 + test/util/run_test_in_process.dart | 64 +++ test/util/timeline_test_helpers.dart | 488 ------------------ 9 files changed, 505 insertions(+), 520 deletions(-) create mode 100644 test/timeline/tap/timeline_tap_test_bodies.dart create mode 100644 test/timeline/timeline_test_shared.dart create mode 100644 test/util/run_test_in_process.dart delete mode 100644 test/util/timeline_test_helpers.dart diff --git a/test/timeline/drag/timeline_drag_test.dart b/test/timeline/drag/timeline_drag_test.dart index 0d670997..bbcca52e 100644 --- a/test/timeline/drag/timeline_drag_test.dart +++ b/test/timeline/drag/timeline_drag_test.dart @@ -5,7 +5,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import 'package:test_process/test_process.dart'; import '../../util/capture_console_output.dart'; -import '../../util/timeline_test_helpers.dart'; +import '../timeline_test_shared.dart' as shared; import 'drag_until_visible_test_widget.dart'; final _firstItemSelector = spotText('Item at index: 3', exact: true); @@ -17,7 +17,6 @@ const _passingDragAmount = 23; const _passingOffset = Offset(0, -2300); const _header = '==================== Timeline Event ===================='; -// const _separator = '========================================================'; String _testAsString({ required String title, @@ -34,7 +33,7 @@ import 'package:spot/src/timeline/timeline.dart';\n $widgetPart\n void main() async { testWidgets("$title", (WidgetTester tester) async { - ${TimelineTestHelpers.localTimelineInitiator(timelineMode)}; + ${shared.localTimelineInitiator(timelineMode)}; await tester.pumpWidget(const DragUntilVisibleTestWidget()); final firstItem = spotText('Item at index: 3', exact: true)..existsOnce(); final secondItem = spotText('Item at index: 27', exact: true) diff --git a/test/timeline/tap/global/global_live_timeline_tap_test.dart b/test/timeline/tap/global/global_live_timeline_tap_test.dart index 6f909d11..8bcadc27 100644 --- a/test/timeline/tap/global/global_live_timeline_tap_test.dart +++ b/test/timeline/tap/global/global_live_timeline_tap_test.dart @@ -1,28 +1,27 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spot/src/timeline/timeline.dart'; - -import '../../../util/timeline_test_helpers.dart'; +import '../timeline_tap_test_bodies.dart' as body; void main() { globalTimelineMode = TimelineMode.live; testWidgets('Global: record, without error', (tester) async { - await TimelineTestHelpers.liveWithoutError( + await body.liveWithoutError( tester: tester, isGlobalMode: true, ); }); test('Global: live - without error, prints HTML', () async { - await TimelineTestHelpers.liveWithoutErrorPrintsHtml( + await body.liveWithoutErrorPrintsHtml( isGlobalMode: true, ); }); test('Global: live - with error, no duplicates, prints HTML', () async { - await TimelineTestHelpers.liveWithErrorNoDuplicatesPrintsHtml( + await body.liveWithErrorNoDuplicatesPrintsHtml( isGlobalMode: true, ); }); testWidgets('Global: live, turn off during test', (tester) async { - await TimelineTestHelpers.liveTurnOffDuringTest( + await body.liveTurnOffDuringTest( isGlobalMode: true, tester: tester, ); diff --git a/test/timeline/tap/global/global_off_timeline_tap_test.dart b/test/timeline/tap/global/global_off_timeline_tap_test.dart index 70c39e82..c03a5e9a 100644 --- a/test/timeline/tap/global/global_off_timeline_tap_test.dart +++ b/test/timeline/tap/global/global_off_timeline_tap_test.dart @@ -1,12 +1,11 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spot/src/timeline/timeline.dart'; - -import '../../../util/timeline_test_helpers.dart'; +import '../timeline_tap_test_bodies.dart' as body; void main() { globalTimelineMode = TimelineMode.off; testWidgets('Global: off, without error', (tester) async { - await TimelineTestHelpers.offWithoutError( + await body.offWithoutError( tester: tester, isGlobalMode: true, ); diff --git a/test/timeline/tap/global/global_record_timeline_tap_test.dart b/test/timeline/tap/global/global_record_timeline_tap_test.dart index c62102e2..27d8f3d9 100644 --- a/test/timeline/tap/global/global_record_timeline_tap_test.dart +++ b/test/timeline/tap/global/global_record_timeline_tap_test.dart @@ -1,17 +1,16 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spot/src/timeline/timeline.dart'; - -import '../../../util/timeline_test_helpers.dart'; +import '../timeline_tap_test_bodies.dart' as body; void main() { globalTimelineMode = TimelineMode.record; testWidgets('Global: record, without error', (tester) async { - await TimelineTestHelpers.recordWithoutError( + await body.recordWithoutError( tester: tester, isGlobalMode: true, ); }); test('Global: record, with error', () async { - await TimelineTestHelpers.recordWithError(isGlobalMode: true); + await body.recordWithError(isGlobalMode: true); }); } diff --git a/test/timeline/tap/local/local_timeline_tap_test.dart b/test/timeline/tap/local/local_timeline_tap_test.dart index 6eebe123..7020f441 100644 --- a/test/timeline/tap/local/local_timeline_tap_test.dart +++ b/test/timeline/tap/local/local_timeline_tap_test.dart @@ -1,42 +1,36 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; -import '../../../util/timeline_test_helpers.dart'; +import '../timeline_tap_test_bodies.dart' as body; void main() { - group('Initial Values', () { - test('Global Timeline', () { - expect(globalTimelineMode, TimelineMode.record); - }); - test('Local Timeline', () { - expect(localTimelineMode, isNull); - }); + test('Initial value', () { + expect(localTimelineMode, isNull); }); - group('Override global timeline', () { testWidgets('Local: live, without error', (tester) async { - await TimelineTestHelpers.liveWithoutError(tester: tester); + await body.liveWithoutError(tester: tester); }); testWidgets('Local: off, without error', (tester) async { - await TimelineTestHelpers.offWithoutError(tester: tester); + await body.offWithoutError(tester: tester); }); testWidgets('Local: live, turn off during test', (tester) async { - await TimelineTestHelpers.liveTurnOffDuringTest(tester: tester); + await body.liveTurnOffDuringTest(tester: tester); }); }); group('Print on teardown', () { testWidgets('Local: record, without error', (tester) async { - await TimelineTestHelpers.recordWithoutError(tester: tester); + await body.recordWithoutError(tester: tester); }); test('Local: record, with error', () async { - await TimelineTestHelpers.recordWithError(); + await body.recordWithError(); }); test('Local: live - without error, prints HTML', () async { - await TimelineTestHelpers.liveWithoutErrorPrintsHtml(); + await body.liveWithoutErrorPrintsHtml(); }); test('Local: live - with error, no duplicates, prints HTML', () async { - await TimelineTestHelpers.liveWithErrorNoDuplicatesPrintsHtml(); + await body.liveWithErrorNoDuplicatesPrintsHtml(); }); }); } diff --git a/test/timeline/tap/timeline_tap_test_bodies.dart b/test/timeline/tap/timeline_tap_test_bodies.dart new file mode 100644 index 00000000..ffac2bdf --- /dev/null +++ b/test/timeline/tap/timeline_tap_test_bodies.dart @@ -0,0 +1,396 @@ +import 'dart:io'; + +import 'package:dartx/dartx.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; + +import '../../util/capture_console_output.dart'; +import '../../util/run_test_in_process.dart' as process; +import '../timeline_test_shared.dart' as shared; +import 'timeline_tap_test_widget.dart'; + +final WidgetSelector _addButtonSelector = spotIcon(Icons.add); +final _subtractButtonSelector = spotIcon(Icons.remove); +final _clearButtonSelector = spotIcon(Icons.clear); +const String counter3 = 'Counter: 3'; +const String counter4 = 'Counter: 4'; +const String counter99 = 'Counter: 99'; + +Future recordWithoutError({ + required WidgetTester tester, + bool isGlobalMode = false, +}) async { + final output = await captureConsoleOutput(() async { + if (!isGlobalMode) { + localTimelineMode = TimelineMode.record; + } + await tester.pumpWidget(const TimelineTestWidget()); + _addButtonSelector.existsOnce(); + spotText('Counter: 3').existsOnce(); + await act.tap(_addButtonSelector); + spotText('Counter: 4').existsOnce(); + await act.tap(_subtractButtonSelector); + spotText('Counter: 3').existsOnce(); +// Notify that the timeline of this type is already recording. + localTimelineMode = TimelineMode.record; + }); + expect(output, contains('馃敶 - Recording error output timeline')); + expect( + output, + isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), + ); + expect( + output, + isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), + ); + expect(output, contains('Timeline mode is already set to "record"')); + _testTimeLineContent(output: output, eventCount: 0); +} + +Future recordWithError({ + bool isGlobalMode = false, +}) async { + final stdout = await _outputFromTapTestProcess( + title: 'OnError timeline - with error, prints timeline', + timelineMode: TimelineMode.record, + shouldFail: true, + isGlobalMode: isGlobalMode, + captureStart: 'Timeline', + ); + + final timeline = stdout.split('\n'); + + expect(timeline.first, 'Timeline'); + expect( + timeline[1], + shared.timelineHeader, + ); + expect( + timeline[2], + 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', + ); + expect( + timeline[3].startsWith('Caller: at main. file:///'), + isTrue, + ); + expect( + timeline[4].startsWith( + 'Screenshot: file:///', + ), + isTrue, + ); + expect( + timeline[5].startsWith( + 'Timestamp:', + ), + isTrue, + ); + expect( + timeline[6], + shared.timelineSeparator, + ); + final prefix = isGlobalMode ? 'global' : 'local'; + final htmlLine = + timeline.firstWhere((line) => line.startsWith('View time line here:')); + expect( + htmlLine.endsWith( + 'timeline-$prefix-onerror-timeline-with-error-prints-timeline.html', + ), + isTrue, + ); +} + +Future liveWithoutErrorPrintsHtml({ + bool isGlobalMode = false, +}) async { + final stdout = await _outputFromTapTestProcess( + title: 'Live timeline without error prints html', + timelineMode: TimelineMode.live, + isGlobalMode: isGlobalMode, + ); + final timeline = stdout.split('\n'); +// Does not start with 'Timeline', this only happens on error + expect(timeline.first, shared.timelineHeader); + expect( + timeline.second, + 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', + ); + expect( + timeline[2].startsWith('Caller: at'), + isTrue, + ); + expect( + timeline[3].startsWith( + 'Screenshot: file:///', + ), + isTrue, + ); + expect( + timeline[4].startsWith( + 'Timestamp:', + ), + isTrue, + ); + expect( + timeline[5], + shared.timelineSeparator, + ); + final htmlLine = + timeline.firstWhere((line) => line.startsWith('View time line here:')); + final prefix = isGlobalMode ? 'global' : 'local'; + expect( + htmlLine.endsWith( + 'timeline-$prefix-live-timeline-without-error-prints-html.html', + ), + isTrue, + ); +} + +Future liveWithErrorNoDuplicatesPrintsHtml({ + bool isGlobalMode = false, +}) async { + final stdout = await _outputFromTapTestProcess( + title: 'Live timeline - with error, no duplicates, prints HTML', + timelineMode: TimelineMode.live, + shouldFail: true, + isGlobalMode: isGlobalMode, + ); + + final timeline = stdout.split('\n'); +// Does not start with 'Timeline', this only happens on error + expect(timeline.first, shared.timelineHeader); + expect( + timeline.second, + 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', + ); + expect( + timeline[2].startsWith('Caller: at main. file:///'), + isTrue, + ); + expect( + timeline[3].startsWith( + 'Screenshot: file:///', + ), + isTrue, + ); + expect( + timeline[4].startsWith( + 'Timestamp:', + ), + isTrue, + ); + expect( + timeline[5], + shared.timelineSeparator, + ); + final prefix = isGlobalMode ? 'global' : 'local'; + final htmlLine = + timeline.firstWhere((line) => line.startsWith('View time line here:')); + expect( + htmlLine.endsWith( + 'timeline-$prefix-live-timeline-with-error-no-duplicates-prints-html.html', + ), + isTrue, + ); +} + +Future offWithoutError({ + required WidgetTester tester, + bool isGlobalMode = false, +}) async { + final output = await captureConsoleOutput(() async { + if (!isGlobalMode) { + localTimelineMode = TimelineMode.off; + } + await tester.pumpWidget(const TimelineTestWidget()); + _addButtonSelector.existsOnce(); + spotText('Counter: 3').existsOnce(); + await act.tap(_addButtonSelector); + spotText('Counter: 4').existsOnce(); + await act.tap(_subtractButtonSelector); + spotText('Counter: 3').existsOnce(); + }); + + expect(output, contains('鈴革笌 - Timeline recording is off')); + expect( + output, + isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), + ); + expect( + output, + isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), + ); + _testTimeLineContent(output: output, eventCount: 0); +} + +Future liveTurnOffDuringTest({ + required WidgetTester tester, + bool isGlobalMode = false, +}) async { + final output = await captureConsoleOutput(() async { + if (!isGlobalMode) { + localTimelineMode = TimelineMode.live; + } + await tester.pumpWidget( + const TimelineTestWidget(), + ); + spotText('Counter: 3').existsOnce(); + _addButtonSelector.existsOnce(); + await act.tap(_addButtonSelector); + spotText('Counter: 4').existsOnce(); + await act.tap(_subtractButtonSelector); + spotText('Counter: 3').existsOnce(); +// Notify that the recording stopped + if (isGlobalMode) { + globalTimelineMode = TimelineMode.off; + } else { + localTimelineMode = TimelineMode.off; + } + await act.tap(_clearButtonSelector); + spotText('Counter: 0').existsOnce(); +// Notify that the recording is already off + if (isGlobalMode) { + globalTimelineMode = TimelineMode.off; + } else { + localTimelineMode = TimelineMode.off; + } + }); + expect(output, contains('馃敶 - Recording live timeline')); + expect( + output, + contains('Tap ${_addButtonSelector.toStringBreadcrumb()}'), + ); + expect( + output, + contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), + ); +// No further events were added to the timeline, including screenshots + expect( + output, + isNot(contains('Tap ${_clearButtonSelector.toStringBreadcrumb()}')), + ); + _testTimeLineContent(output: output, eventCount: 2); + expect(output, contains('鈴革笌 - Timeline recording is off')); + expect(output, contains('Timeline mode is already set to "off"')); +} + +Future liveWithoutError({ + required WidgetTester tester, + bool isGlobalMode = false, +}) async { + final output = await captureConsoleOutput(() async { + if (!isGlobalMode) { + localTimelineMode = TimelineMode.live; + } + await tester.pumpWidget(const TimelineTestWidget()); + _addButtonSelector.existsOnce(); + spotText('Counter: 3').existsOnce(); + await act.tap(_addButtonSelector); + spotText('Counter: 4').existsOnce(); + await act.tap(_subtractButtonSelector); + spotText('Counter: 3').existsOnce(); +// Notify that the timeline mode is already set to live + localTimelineMode = TimelineMode.live; + }); + expect(output, contains('馃敶 - Recording live timeline')); + expect( + output, + contains('Event: Tap ${_addButtonSelector.toStringBreadcrumb()}'), + ); + expect( + output, + contains('Event: Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), + ); + expect(output, contains('Timeline mode is already set to "live"')); + _testTimeLineContent(output: output, eventCount: 2); +} + +void _testTimeLineContent({ + required String output, + required int eventCount, +}) { + expect( + RegExp(shared.timelineHeader).allMatches(output).length, + eventCount, + ); + expect( + RegExp('Event: Tap Icon Widget with icon:').allMatches(output).length, + eventCount, + ); + final callerParts = output.split('\n').where((line) { + return line.startsWith('Caller: at') && line.contains('file://'); + }).toList(); + expect( + callerParts.length, + eventCount, + ); + expect( + RegExp('Screenshot: file:').allMatches(output).length, + eventCount, + ); + expect( + RegExp('Timestamp: ').allMatches(output).length, + eventCount, + ); +} + +String _tapTestAsString({ + required String title, + required TimelineMode timelineMode, + bool shouldFail = false, + bool isGlobalMode = false, +}) { + final testTitle = '${isGlobalMode ? 'Global: ' : 'Local: '}$title'; + + final globalInitiator = + isGlobalMode ? '${shared.globalTimelineInitiator(timelineMode)};' : ''; + + final localInitiator = + isGlobalMode ? '' : '${shared.localTimelineInitiator(timelineMode)};'; + + final widgetPart = File('test/timeline/tap/timeline_tap_test_widget.dart') + .readAsStringSync(); + + return ''' +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; +import 'package:spot/src/timeline/timeline.dart';\n +$widgetPart\n +void main() async { + $globalInitiator + final addButtonSelector = spotIcon(Icons.add); + final subtractButtonSelector = spotIcon(Icons.remove); + testWidgets("$testTitle", (WidgetTester tester) async { + $localInitiator + await tester.pumpWidget(const TimelineTestWidget()); + addButtonSelector.existsOnce(); + spotText('Counter: 3').existsOnce(); + await act.tap(addButtonSelector); + spotText('Counter: 4').existsOnce(); + await act.tap(subtractButtonSelector); + spotText('Counter: 3').existsOnce(); + ${shouldFail ? 'spotText("Counter: 99").existsOnce();' : ''} + }); +} +'''; +} + +Future _outputFromTapTestProcess({ + required String title, + required TimelineMode timelineMode, + String captureStart = shared.timelineHeader, + bool shouldFail = false, + bool isGlobalMode = false, +}) async { + return process.runTestInProcessAndCaptureOutPut( + shouldFail: shouldFail, + testAsString: _tapTestAsString( + title: title, + timelineMode: timelineMode, + shouldFail: shouldFail, + isGlobalMode: isGlobalMode, + ), + captureStart: captureStart, + ); +} diff --git a/test/timeline/timeline_test_shared.dart b/test/timeline/timeline_test_shared.dart new file mode 100644 index 00000000..00af1079 --- /dev/null +++ b/test/timeline/timeline_test_shared.dart @@ -0,0 +1,23 @@ +import 'package:spot/spot.dart'; + +const String timelineHeader = + '==================== Timeline Event ===================='; + +const String timelineSeparator = + '========================================================'; + +String localTimelineInitiator(TimelineMode timelineMode) { + return switch (timelineMode) { + TimelineMode.live => 'localTimelineMode = TimelineMode.live;', + TimelineMode.record => 'localTimelineMode = TimelineMode.record;', + TimelineMode.off => 'localTimelineMode = TimelineMode.off;', + }; +} + +String globalTimelineInitiator(TimelineMode timelineMode) { + return switch (timelineMode) { + TimelineMode.live => 'globalTimelineMode = TimelineMode.live;', + TimelineMode.record => 'globalTimelineMode = TimelineMode.record;', + TimelineMode.off => 'globalTimelineMode = TimelineMode.off;', + }; +} diff --git a/test/util/run_test_in_process.dart b/test/util/run_test_in_process.dart new file mode 100644 index 00000000..457fa195 --- /dev/null +++ b/test/util/run_test_in_process.dart @@ -0,0 +1,64 @@ +import 'dart:io'; + +import 'package:test_process/test_process.dart'; + +/// Runs a Flutter test in a new process and captures its output. +/// +/// This function creates a temporary test file with the provided test code, +/// starts a new Flutter test process with the specified arguments, captures +/// the output of the process, and returns the captured output as a string. +/// The temporary test file is deleted after the test process completes. +/// +/// The `args` parameter allows additional arguments to be passed to the test +/// process. The `'test'` argument is always included automatically and should +/// not be repeated in the `args` list. If `captureStart` is provided, the +/// output will be captured starting from the line that matches `captureStart`. +Future runTestInProcessAndCaptureOutPut({ + required String testAsString, + String? captureStart, + bool shouldFail = false, + Iterable? args, +}) async { + final tempTestFile = await _createTempTestFile(testAsString); + + final arguments = [ + 'test', + tempTestFile.path, + ...?args?.where((arg) => arg != 'test'), + ]; + + final testProcess = await TestProcess.start('flutter', arguments); + final stdoutBuffer = StringBuffer(); + bool write = captureStart == null; + + await for (final line in testProcess.stdoutStream()) { + if (line.isEmpty) continue; + if (!write && line == captureStart) { + write = true; + } + if (write) { + stdoutBuffer.writeln(line); + } + } + + await testProcess.shouldExit(shouldFail ? 1 : 0); + + final stdout = stdoutBuffer.toString(); + + _deleteTempDir(tempTestFile.parent); + + return stdout; +} + +Future _createTempTestFile(String content) async { + final tempDir = Directory.systemTemp.createTempSync(); + final tempTestFile = File('${tempDir.path}/temp_test.dart'); + await tempTestFile.writeAsString(content); + return tempTestFile; +} + +void _deleteTempDir(Directory tempDir) { + if (tempDir.existsSync()) { + tempDir.deleteSync(recursive: true); + } +} diff --git a/test/util/timeline_test_helpers.dart b/test/util/timeline_test_helpers.dart deleted file mode 100644 index 7494699d..00000000 --- a/test/util/timeline_test_helpers.dart +++ /dev/null @@ -1,488 +0,0 @@ -import 'dart:io'; - -import 'package:dartx/dartx.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:spot/spot.dart'; -import 'package:test_process/test_process.dart'; - -import '../timeline/tap/timeline_tap_test_widget.dart'; -import 'capture_console_output.dart'; - -class TimelineTestHelpers { - static final WidgetSelector _addButtonSelector = spotIcon(Icons.add); - static final _subtractButtonSelector = spotIcon(Icons.remove); - static final _clearButtonSelector = spotIcon(Icons.clear); - - static const _header = - '==================== Timeline Event ===================='; - static const _separator = - '========================================================'; - - static String localTimelineInitiator(TimelineMode timelineMode) { - return switch (timelineMode) { - TimelineMode.live => 'localTimelineMode = TimelineMode.live;', - TimelineMode.record => 'localTimelineMode = TimelineMode.record;', - TimelineMode.off => 'localTimelineMode = TimelineMode.off;', - }; - } - - static String _globalTimelineInitiator(TimelineMode timelineMode) { - return switch (timelineMode) { - TimelineMode.live => 'globalTimelineMode = TimelineMode.live;', - TimelineMode.record => 'globalTimelineMode = TimelineMode.record;', - TimelineMode.off => 'globalTimelineMode = TimelineMode.off;', - }; - } - - static Future recordWithoutError({ - required WidgetTester tester, - bool isGlobalMode = false, - }) async { - final output = await captureConsoleOutput(() async { - if (!isGlobalMode) { - localTimelineMode = TimelineMode.record; - } - await tester.pumpWidget(const TimelineTestWidget()); - _addButtonSelector.existsOnce(); - spotText('Counter: 3').existsOnce(); - await act.tap(_addButtonSelector); - spotText('Counter: 4').existsOnce(); - await act.tap(_subtractButtonSelector); - spotText('Counter: 3').existsOnce(); - // Notify that the timeline of this type is already recording. - localTimelineMode = TimelineMode.record; - }); - expect(output, contains('馃敶 - Recording error output timeline')); - expect( - output, - isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), - ); - expect( - output, - isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), - ); - expect(output, contains('Timeline mode is already set to "record"')); - _testTimeLineContent(output: output, eventCount: 0); - } - - static Future recordWithError({ - bool isGlobalMode = false, - }) async { - final tempDir = Directory.systemTemp.createTempSync(); - final tempTestFile = File('${tempDir.path}/temp_test.dart'); - await tempTestFile.writeAsString( - _tapTestAsString( - title: 'OnError timeline - with error, prints timeline', - timelineMode: TimelineMode.record, - shouldFail: true, - isGlobalMode: isGlobalMode, - ), - ); - - final testProcess = - await TestProcess.start('flutter', ['test', tempTestFile.path]); - - final stdoutBuffer = StringBuffer(); - - bool write = false; - await for (final line in testProcess.stdoutStream()) { - if (line.isEmpty) continue; - if (line == 'Timeline') { - write = true; - } - if (write) { - stdoutBuffer.writeln(line); - } - } - - // Error happens - await testProcess.shouldExit(1); - - if (tempDir.existsSync()) { - tempDir.deleteSync(recursive: true); - } - - final stdout = stdoutBuffer.toString(); - final timeline = stdout.split('\n'); - - expect(timeline.first, 'Timeline'); - expect( - timeline[1], - _header, - ); - expect( - timeline[2], - 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', - ); - expect( - timeline[3].startsWith('Caller: at main. file:///'), - isTrue, - ); - expect( - timeline[4].startsWith( - 'Screenshot: file:///', - ), - isTrue, - ); - expect( - timeline[5].startsWith( - 'Timestamp:', - ), - isTrue, - ); - expect( - timeline[6], - _separator, - ); - final prefix = isGlobalMode ? 'global' : 'local'; - final htmlLine = - timeline.firstWhere((line) => line.startsWith('View time line here:')); - expect( - htmlLine.endsWith( - 'timeline-$prefix-onerror-timeline-with-error-prints-timeline.html', - ), - isTrue, - ); - } - - static Future liveWithoutErrorPrintsHtml({ - bool isGlobalMode = false, - }) async { - final tempDir = Directory.systemTemp.createTempSync(); - final tempTestFile = File('${tempDir.path}/temp_test.dart'); - await tempTestFile.writeAsString( - _tapTestAsString( - title: 'Live timeline without error prints html', - timelineMode: TimelineMode.live, - isGlobalMode: isGlobalMode, - ), - ); - - final testProcess = - await TestProcess.start('flutter', ['test', tempTestFile.path]); - - final stdoutBuffer = StringBuffer(); - - bool write = false; - await for (final line in testProcess.stdoutStream()) { - if (line.isEmpty) continue; - - if (!write) { - if (line == _header) { - write = true; - } - } - - if (write) { - stdoutBuffer.writeln(line); - } - } - - // Error does not happen - await testProcess.shouldExit(0); - - if (tempDir.existsSync()) { - tempDir.deleteSync(recursive: true); - } - - final stdout = stdoutBuffer.toString(); - final timeline = stdout.split('\n'); - // Does not start with 'Timeline', this only happens on error - expect(timeline.first, _header); - expect( - timeline.second, - 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', - ); - expect( - timeline[2].startsWith('Caller: at'), - isTrue, - ); - expect( - timeline[3].startsWith( - 'Screenshot: file:///', - ), - isTrue, - ); - expect( - timeline[4].startsWith( - 'Timestamp:', - ), - isTrue, - ); - expect( - timeline[5], - _separator, - ); - final htmlLine = - timeline.firstWhere((line) => line.startsWith('View time line here:')); - final prefix = isGlobalMode ? 'global' : 'local'; - expect( - htmlLine.endsWith( - 'timeline-$prefix-live-timeline-without-error-prints-html.html', - ), - isTrue, - ); - } - - static Future liveWithErrorNoDuplicatesPrintsHtml({ - bool isGlobalMode = false, - }) async { - final tempDir = Directory.systemTemp.createTempSync(); - final tempTestFile = File('${tempDir.path}/temp_test.dart'); - await tempTestFile.writeAsString( - _tapTestAsString( - title: 'Live timeline - with error, no duplicates, prints HTML', - timelineMode: TimelineMode.live, - shouldFail: true, - isGlobalMode: isGlobalMode, - ), - ); - - final testProcess = - await TestProcess.start('flutter', ['test', tempTestFile.path]); - - final stdoutBuffer = StringBuffer(); - - bool write = false; - await for (final line in testProcess.stdoutStream()) { - if (line.isEmpty) continue; - - if (!write) { - if (line == _header) { - write = true; - } - } - - if (write) { - stdoutBuffer.writeln(line); - } - } - - // Error does not happen - await testProcess.shouldExit(1); - - if (tempDir.existsSync()) { - tempDir.deleteSync(recursive: true); - } - - final stdout = stdoutBuffer.toString(); - final timeline = stdout.split('\n'); - // Does not start with 'Timeline', this only happens on error - expect(timeline.first, _header); - expect( - timeline.second, - 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', - ); - expect( - timeline[2].startsWith('Caller: at main. file:///'), - isTrue, - ); - expect( - timeline[3].startsWith( - 'Screenshot: file:///', - ), - isTrue, - ); - expect( - timeline[4].startsWith( - 'Timestamp:', - ), - isTrue, - ); - expect( - timeline[5], - _separator, - ); - final prefix = isGlobalMode ? 'global' : 'local'; - final htmlLine = - timeline.firstWhere((line) => line.startsWith('View time line here:')); - expect( - htmlLine.endsWith( - 'timeline-$prefix-live-timeline-with-error-no-duplicates-prints-html.html', - ), - isTrue, - ); - } - - static Future offWithoutError({ - required WidgetTester tester, - bool isGlobalMode = false, - }) async { - final output = await captureConsoleOutput(() async { - if (!isGlobalMode) { - localTimelineMode = TimelineMode.off; - } - await tester.pumpWidget(const TimelineTestWidget()); - _addButtonSelector.existsOnce(); - spotText('Counter: 3').existsOnce(); - await act.tap(_addButtonSelector); - spotText('Counter: 4').existsOnce(); - await act.tap(_subtractButtonSelector); - spotText('Counter: 3').existsOnce(); - }); - - expect(output, contains('鈴革笌 - Timeline recording is off')); - expect( - output, - isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), - ); - expect( - output, - isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), - ); - _testTimeLineContent(output: output, eventCount: 0); - } - - static Future liveTurnOffDuringTest({ - required WidgetTester tester, - bool isGlobalMode = false, - }) async { - final output = await captureConsoleOutput(() async { - if (!isGlobalMode) { - localTimelineMode = TimelineMode.live; - } - await tester.pumpWidget( - const TimelineTestWidget(), - ); - spotText('Counter: 3').existsOnce(); - _addButtonSelector.existsOnce(); - await act.tap(_addButtonSelector); - spotText('Counter: 4').existsOnce(); - await act.tap(_subtractButtonSelector); - spotText('Counter: 3').existsOnce(); - // Notify that the recording stopped - if (isGlobalMode) { - globalTimelineMode = TimelineMode.off; - } else { - localTimelineMode = TimelineMode.off; - } - await act.tap(_clearButtonSelector); - spotText('Counter: 0').existsOnce(); - // Notify that the recording is already off - if (isGlobalMode) { - globalTimelineMode = TimelineMode.off; - } else { - localTimelineMode = TimelineMode.off; - } - }); - expect(output, contains('馃敶 - Recording live timeline')); - expect( - output, - contains('Tap ${_addButtonSelector.toStringBreadcrumb()}'), - ); - expect( - output, - contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), - ); - // No further events were added to the timeline, including screenshots - expect( - output, - isNot(contains('Tap ${_clearButtonSelector.toStringBreadcrumb()}')), - ); - _testTimeLineContent(output: output, eventCount: 2); - expect(output, contains('鈴革笌 - Timeline recording is off')); - expect(output, contains('Timeline mode is already set to "off"')); - } - - static Future liveWithoutError({ - required WidgetTester tester, - bool isGlobalMode = false, - }) async { - final output = await captureConsoleOutput(() async { - if (!isGlobalMode) { - localTimelineMode = TimelineMode.live; - } - await tester.pumpWidget(const TimelineTestWidget()); - _addButtonSelector.existsOnce(); - spotText('Counter: 3').existsOnce(); - await act.tap(_addButtonSelector); - spotText('Counter: 4').existsOnce(); - await act.tap(_subtractButtonSelector); - spotText('Counter: 3').existsOnce(); - // Notify that the timeline mode is already set to live - localTimelineMode = TimelineMode.live; - }); - expect(output, contains('馃敶 - Recording live timeline')); - expect( - output, - contains('Event: Tap ${_addButtonSelector.toStringBreadcrumb()}'), - ); - expect( - output, - contains('Event: Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), - ); - expect(output, contains('Timeline mode is already set to "live"')); - _testTimeLineContent(output: output, eventCount: 2); - } - - static void _testTimeLineContent({ - required String output, - required int eventCount, - }) { - expect( - RegExp(_header).allMatches(output).length, - eventCount, - ); - expect( - RegExp('Event: Tap Icon Widget with icon:').allMatches(output).length, - eventCount, - ); - final callerParts = output.split('\n').where((line) { - return line.startsWith('Caller: at') && line.contains('file://'); - }).toList(); - expect( - callerParts.length, - eventCount, - ); - expect( - RegExp('Screenshot: file:').allMatches(output).length, - eventCount, - ); - expect( - RegExp('Timestamp: ').allMatches(output).length, - eventCount, - ); - } - - static String _tapTestAsString({ - required String title, - required TimelineMode timelineMode, - bool shouldFail = false, - bool isGlobalMode = false, - }) { - final testTitle = '${isGlobalMode ? 'Global: ' : 'Local: '}$title'; - - final globalInitiator = - isGlobalMode ? '${_globalTimelineInitiator(timelineMode)};' : ''; - - final localInitiator = - isGlobalMode ? '' : '${localTimelineInitiator(timelineMode)};'; - - final widgetPart = File('test/timeline/tap/timeline_tap_test_widget.dart') - .readAsStringSync(); - - return ''' -import 'package:flutter_test/flutter_test.dart'; -import 'package:spot/spot.dart'; -import 'package:spot/src/timeline/timeline.dart';\n -$widgetPart\n -void main() async { - $globalInitiator - final addButtonSelector = spotIcon(Icons.add); - final subtractButtonSelector = spotIcon(Icons.remove); - testWidgets("$testTitle", (WidgetTester tester) async { - $localInitiator - await tester.pumpWidget(const TimelineTestWidget()); - addButtonSelector.existsOnce(); - spotText('Counter: 3').existsOnce(); - await act.tap(addButtonSelector); - spotText('Counter: 4').existsOnce(); - await act.tap(subtractButtonSelector); - spotText('Counter: 3').existsOnce(); - ${shouldFail ? 'spotText("Counter: 99").existsOnce();' : ''} - }); -} -'''; - } -} From c72ae712a58764c9331c62789b95cffed0a65125 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 26 Jun 2024 16:28:44 +0200 Subject: [PATCH 100/119] Rm localTimelineMode --- lib/src/timeline/timeline.dart | 43 +++++++------------ test/act/act_drag_test.dart | 2 +- test/timeline/drag/timeline_drag_test.dart | 16 +++---- .../tap/local/local_timeline_tap_test.dart | 4 -- .../tap/timeline_tap_test_bodies.dart | 22 +++++----- test/timeline/timeline_test_shared.dart | 6 +-- test/util/run_test_in_process.dart | 1 - 7 files changed, 38 insertions(+), 56 deletions(-) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index fb0b8e2e..db0eab0e 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -50,31 +50,6 @@ set globalTimelineMode(TimelineMode value) { } } -/// ... -TimelineMode? _localTimelineMode; - -/// Returns the local timeline mode used within a test. -TimelineMode? get localTimelineMode => _localTimelineMode; - -/// Sets the local timeline mode used within a test. -set localTimelineMode(TimelineMode? value) { - final currentTimelineMode = _localTimelineMode; - _localTimelineMode = value; - if (value != null) { - // ignore: avoid_print - if (value == globalTimelineMode || - (currentTimelineMode != null && value == timeline.mode)) { - // ignore: avoid_print - print('Timeline mode is already set to "${value.name}"'); - return; - } else if (currentTimelineMode != null && currentTimelineMode != value) { - // ignore: avoid_print - print(value.message); - } - timeline.mode = value; - } -} - /// Use --dart-define=SPOT_TIMELINE_MODE=live|record|off to set the [TimlineMode] /// for all tests TimelineMode? getTimelineModeFromEnv() { @@ -104,7 +79,7 @@ Timeline get timeline { // create new timeline final newTimeline = Timeline(); - newTimeline.mode = _localTimelineMode ?? _globalTimelineMode; + newTimeline.mode = _globalTimelineMode; // ignore: avoid_print print(newTimeline.mode.message); @@ -119,7 +94,6 @@ Timeline get timeline { newTimeline._printHTML(); } _timelines.remove(test); - _localTimelineMode = null; }); _timelines[test] = newTimeline; @@ -144,8 +118,21 @@ Timeline get timeline { class Timeline { final List _events = []; + TimelineMode _mode = TimelineMode.off; + /// The mode of the timeline. Defaults to [TimelineMode.off]. - TimelineMode mode = TimelineMode.off; + TimelineMode get mode => _mode; + + set mode(TimelineMode value) { + if (value == _mode) { + // ignore: avoid_print + print('Timeline mode is already set to "${value.name}"'); + return; + } + _mode = value; + // ignore: avoid_print + print(value.message); + } /// Adds a screenshot to the timeline. void addScreenshot( diff --git a/test/act/act_drag_test.dart b/test/act/act_drag_test.dart index c545d92a..de4e5686 100644 --- a/test/act/act_drag_test.dart +++ b/test/act/act_drag_test.dart @@ -6,7 +6,7 @@ import '../timeline/drag/drag_until_visible_test_widget.dart'; void main() { group('Drag Events', () { testWidgets('Finds widget after dragging', (tester) async { - localTimelineMode = TimelineMode.live; + timeline.mode = TimelineMode.live; await tester.pumpWidget( const DragUntilVisibleTestWidget(), ); diff --git a/test/timeline/drag/timeline_drag_test.dart b/test/timeline/drag/timeline_drag_test.dart index bbcca52e..bdc7fddf 100644 --- a/test/timeline/drag/timeline_drag_test.dart +++ b/test/timeline/drag/timeline_drag_test.dart @@ -68,10 +68,10 @@ void main() { group('Without error', () { testWidgets('Drag Until Visible - Live timeline', (tester) async { final output = await captureConsoleOutput(() async { - localTimelineMode = TimelineMode.live; + timeline.mode = TimelineMode.live; await _testBody(tester); // Notify that the timeline of this type is already recording. - localTimelineMode = TimelineMode.live; + timeline.mode = TimelineMode.live; }); expect(output, contains('馃敶 - Now recording live timeline')); _testTimeLineContent( @@ -83,7 +83,7 @@ void main() { }); testWidgets('Start with Timeline Mode off', (tester) async { final output = await captureConsoleOutput(() async { - localTimelineMode = TimelineMode.off; + timeline.mode = TimelineMode.off; await _testBody(tester); }); final splitted = output.split('\n') @@ -94,11 +94,11 @@ void main() { }); testWidgets('Turn timeline mode off during test', (tester) async { final output = await captureConsoleOutput(() async { - localTimelineMode = TimelineMode.record; + timeline.mode = TimelineMode.record; await _testBody(tester); // Notify that the recording is off - localTimelineMode = TimelineMode.off; - localTimelineMode = TimelineMode.off; + timeline.mode = TimelineMode.off; + timeline.mode = TimelineMode.off; }); expect(output, contains('馃敶 - Now recording live timeline')); @@ -113,9 +113,9 @@ void main() { }); testWidgets('act.drag: OnError timeline - without error', (tester) async { final output = await captureConsoleOutput(() async { - localTimelineMode = TimelineMode.record; + timeline.mode = TimelineMode.record; await _testBody(tester); - localTimelineMode = TimelineMode.record; + timeline.mode = TimelineMode.record; }); final lines = output.split('\n')..removeWhere((line) => line.isEmpty); expect(lines.first, '馃敶 - Now recording error output timeline'); diff --git a/test/timeline/tap/local/local_timeline_tap_test.dart b/test/timeline/tap/local/local_timeline_tap_test.dart index 7020f441..26e0dd32 100644 --- a/test/timeline/tap/local/local_timeline_tap_test.dart +++ b/test/timeline/tap/local/local_timeline_tap_test.dart @@ -1,11 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:spot/spot.dart'; import '../timeline_tap_test_bodies.dart' as body; void main() { - test('Initial value', () { - expect(localTimelineMode, isNull); - }); group('Override global timeline', () { testWidgets('Local: live, without error', (tester) async { await body.liveWithoutError(tester: tester); diff --git a/test/timeline/tap/timeline_tap_test_bodies.dart b/test/timeline/tap/timeline_tap_test_bodies.dart index ffac2bdf..73a30afc 100644 --- a/test/timeline/tap/timeline_tap_test_bodies.dart +++ b/test/timeline/tap/timeline_tap_test_bodies.dart @@ -23,7 +23,7 @@ Future recordWithoutError({ }) async { final output = await captureConsoleOutput(() async { if (!isGlobalMode) { - localTimelineMode = TimelineMode.record; + timeline.mode = TimelineMode.record; } await tester.pumpWidget(const TimelineTestWidget()); _addButtonSelector.existsOnce(); @@ -33,7 +33,7 @@ Future recordWithoutError({ await act.tap(_subtractButtonSelector); spotText('Counter: 3').existsOnce(); // Notify that the timeline of this type is already recording. - localTimelineMode = TimelineMode.record; + timeline.mode = TimelineMode.record; }); expect(output, contains('馃敶 - Recording error output timeline')); expect( @@ -109,6 +109,7 @@ Future liveWithoutErrorPrintsHtml({ timelineMode: TimelineMode.live, isGlobalMode: isGlobalMode, ); + final timeline = stdout.split('\n'); // Does not start with 'Timeline', this only happens on error expect(timeline.first, shared.timelineHeader); @@ -201,7 +202,7 @@ Future offWithoutError({ }) async { final output = await captureConsoleOutput(() async { if (!isGlobalMode) { - localTimelineMode = TimelineMode.off; + timeline.mode = TimelineMode.off; } await tester.pumpWidget(const TimelineTestWidget()); _addButtonSelector.existsOnce(); @@ -230,7 +231,7 @@ Future liveTurnOffDuringTest({ }) async { final output = await captureConsoleOutput(() async { if (!isGlobalMode) { - localTimelineMode = TimelineMode.live; + timeline.mode = TimelineMode.live; } await tester.pumpWidget( const TimelineTestWidget(), @@ -245,7 +246,7 @@ Future liveTurnOffDuringTest({ if (isGlobalMode) { globalTimelineMode = TimelineMode.off; } else { - localTimelineMode = TimelineMode.off; + timeline.mode = TimelineMode.off; } await act.tap(_clearButtonSelector); spotText('Counter: 0').existsOnce(); @@ -253,7 +254,7 @@ Future liveTurnOffDuringTest({ if (isGlobalMode) { globalTimelineMode = TimelineMode.off; } else { - localTimelineMode = TimelineMode.off; + timeline.mode = TimelineMode.off; } }); expect(output, contains('馃敶 - Recording live timeline')); @@ -281,7 +282,7 @@ Future liveWithoutError({ }) async { final output = await captureConsoleOutput(() async { if (!isGlobalMode) { - localTimelineMode = TimelineMode.live; + timeline.mode = TimelineMode.live; } await tester.pumpWidget(const TimelineTestWidget()); _addButtonSelector.existsOnce(); @@ -291,7 +292,7 @@ Future liveWithoutError({ await act.tap(_subtractButtonSelector); spotText('Counter: 3').existsOnce(); // Notify that the timeline mode is already set to live - localTimelineMode = TimelineMode.live; + timeline.mode = TimelineMode.live; }); expect(output, contains('馃敶 - Recording live timeline')); expect( @@ -344,14 +345,13 @@ String _tapTestAsString({ final testTitle = '${isGlobalMode ? 'Global: ' : 'Local: '}$title'; final globalInitiator = - isGlobalMode ? '${shared.globalTimelineInitiator(timelineMode)};' : ''; + isGlobalMode ? shared.globalTimelineInitiator(timelineMode) : ''; final localInitiator = - isGlobalMode ? '' : '${shared.localTimelineInitiator(timelineMode)};'; + isGlobalMode ? '' : shared.localTimelineInitiator(timelineMode); final widgetPart = File('test/timeline/tap/timeline_tap_test_widget.dart') .readAsStringSync(); - return ''' import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; diff --git a/test/timeline/timeline_test_shared.dart b/test/timeline/timeline_test_shared.dart index 00af1079..2ef951ab 100644 --- a/test/timeline/timeline_test_shared.dart +++ b/test/timeline/timeline_test_shared.dart @@ -8,9 +8,9 @@ const String timelineSeparator = String localTimelineInitiator(TimelineMode timelineMode) { return switch (timelineMode) { - TimelineMode.live => 'localTimelineMode = TimelineMode.live;', - TimelineMode.record => 'localTimelineMode = TimelineMode.record;', - TimelineMode.off => 'localTimelineMode = TimelineMode.off;', + TimelineMode.live => 'timeline.mode = TimelineMode.live;', + TimelineMode.record => 'timeline.mode = TimelineMode.record;', + TimelineMode.off => 'timeline.mode = TimelineMode.off;', }; } diff --git a/test/util/run_test_in_process.dart b/test/util/run_test_in_process.dart index 457fa195..3e1953e0 100644 --- a/test/util/run_test_in_process.dart +++ b/test/util/run_test_in_process.dart @@ -46,7 +46,6 @@ Future runTestInProcessAndCaptureOutPut({ final stdout = stdoutBuffer.toString(); _deleteTempDir(tempTestFile.parent); - return stdout; } From 4766fe06a63c8540d866c88d13850584f6ea3d42 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 26 Jun 2024 17:11:12 +0200 Subject: [PATCH 101/119] Only print message on timelineMode change --- lib/src/timeline/timeline.dart | 19 ++--------- .../tap/timeline_tap_test_bodies.dart | 32 +++++++------------ test/timeline/timeline_test_shared.dart | 9 ++++-- 3 files changed, 20 insertions(+), 40 deletions(-) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index db0eab0e..ca85bb8a 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -34,15 +34,6 @@ TimelineMode _globalTimelineMode = TimelineMode get globalTimelineMode => _globalTimelineMode; set globalTimelineMode(TimelineMode value) { - // ignore: avoid_print - if (value == _globalTimelineMode) { - // ignore: avoid_print - print('Timeline mode is already set to "${value.name}"'); - return; - } else if (_globalTimelineMode != value) { - // ignore: avoid_print - print(value.message); - } _globalTimelineMode = value; final test = Invoker.current?.liveTest; if (test != null) { @@ -79,9 +70,6 @@ Timeline get timeline { // create new timeline final newTimeline = Timeline(); - newTimeline.mode = _globalTimelineMode; - // ignore: avoid_print - print(newTimeline.mode.message); Invoker.current!.addTearDown(() { if (!test.state.result.isPassing && @@ -118,20 +106,19 @@ Timeline get timeline { class Timeline { final List _events = []; - TimelineMode _mode = TimelineMode.off; + TimelineMode _mode = _globalTimelineMode; /// The mode of the timeline. Defaults to [TimelineMode.off]. TimelineMode get mode => _mode; set mode(TimelineMode value) { if (value == _mode) { - // ignore: avoid_print - print('Timeline mode is already set to "${value.name}"'); return; } - _mode = value; // ignore: avoid_print print(value.message); + + _mode = value; } /// Adds a screenshot to the timeline. diff --git a/test/timeline/tap/timeline_tap_test_bodies.dart b/test/timeline/tap/timeline_tap_test_bodies.dart index 73a30afc..9ff2dc7c 100644 --- a/test/timeline/tap/timeline_tap_test_bodies.dart +++ b/test/timeline/tap/timeline_tap_test_bodies.dart @@ -32,10 +32,8 @@ Future recordWithoutError({ spotText('Counter: 4').existsOnce(); await act.tap(_subtractButtonSelector); spotText('Counter: 3').existsOnce(); -// Notify that the timeline of this type is already recording. - timeline.mode = TimelineMode.record; }); - expect(output, contains('馃敶 - Recording error output timeline')); + expect(output, isNot(contains('馃敶 - Recording error output timeline'))); expect( output, isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), @@ -44,7 +42,6 @@ Future recordWithoutError({ output, isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), ); - expect(output, contains('Timeline mode is already set to "record"')); _testTimeLineContent(output: output, eventCount: 0); } @@ -243,21 +240,15 @@ Future liveTurnOffDuringTest({ await act.tap(_subtractButtonSelector); spotText('Counter: 3').existsOnce(); // Notify that the recording stopped - if (isGlobalMode) { - globalTimelineMode = TimelineMode.off; - } else { - timeline.mode = TimelineMode.off; - } + timeline.mode = TimelineMode.off; await act.tap(_clearButtonSelector); spotText('Counter: 0').existsOnce(); -// Notify that the recording is already off - if (isGlobalMode) { - globalTimelineMode = TimelineMode.off; - } else { - timeline.mode = TimelineMode.off; - } }); - expect(output, contains('馃敶 - Recording live timeline')); + final containsMessage = output.contains('馃敶 - Recording live timeline'); + // Changes in local test since it's `record` by default. Globally it does not + // change since the global mode is already `live`. + expect(containsMessage, isGlobalMode ? isFalse : isTrue); + expect(output, contains('鈴革笌 - Timeline recording is off')); expect( output, contains('Tap ${_addButtonSelector.toStringBreadcrumb()}'), @@ -273,7 +264,6 @@ Future liveTurnOffDuringTest({ ); _testTimeLineContent(output: output, eventCount: 2); expect(output, contains('鈴革笌 - Timeline recording is off')); - expect(output, contains('Timeline mode is already set to "off"')); } Future liveWithoutError({ @@ -291,10 +281,11 @@ Future liveWithoutError({ spotText('Counter: 4').existsOnce(); await act.tap(_subtractButtonSelector); spotText('Counter: 3').existsOnce(); -// Notify that the timeline mode is already set to live - timeline.mode = TimelineMode.live; }); - expect(output, contains('馃敶 - Recording live timeline')); + final containsMessage = output.contains('馃敶 - Recording live timeline'); + // Changes in local test since it's `record` by default. Globally it does not + // change since the global mode is already `live`. + expect(containsMessage, isGlobalMode ? isFalse : isTrue); expect( output, contains('Event: Tap ${_addButtonSelector.toStringBreadcrumb()}'), @@ -303,7 +294,6 @@ Future liveWithoutError({ output, contains('Event: Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), ); - expect(output, contains('Timeline mode is already set to "live"')); _testTimeLineContent(output: output, eventCount: 2); } diff --git a/test/timeline/timeline_test_shared.dart b/test/timeline/timeline_test_shared.dart index 2ef951ab..a86bae69 100644 --- a/test/timeline/timeline_test_shared.dart +++ b/test/timeline/timeline_test_shared.dart @@ -8,9 +8,12 @@ const String timelineSeparator = String localTimelineInitiator(TimelineMode timelineMode) { return switch (timelineMode) { - TimelineMode.live => 'timeline.mode = TimelineMode.live;', - TimelineMode.record => 'timeline.mode = TimelineMode.record;', - TimelineMode.off => 'timeline.mode = TimelineMode.off;', + TimelineMode.live => + 'timeline.mode = TimelineMode.live;\nexpect(timeline.mode, TimelineMode.live);', + TimelineMode.record => + 'timeline.mode = TimelineMode.record;\nexpect(timeline.mode, TimelineMode.record);', + TimelineMode.off => + 'timeline.mode = TimelineMode.off;\nexpect(timeline.mode, TimelineMode.off);', }; } From 2cb12e140149d37ac1c78b98580242707ba672e4 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 26 Jun 2024 18:14:14 +0200 Subject: [PATCH 102/119] Add test "Throws when global mode is changed during a test" --- lib/src/timeline/timeline.dart | 8 +++- .../tap/local/local_timeline_tap_test.dart | 8 ++++ .../tap/timeline_tap_test_bodies.dart | 43 ++++++++++++++++--- 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index ca85bb8a..5ac9565e 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -34,11 +34,15 @@ TimelineMode _globalTimelineMode = TimelineMode get globalTimelineMode => _globalTimelineMode; set globalTimelineMode(TimelineMode value) { - _globalTimelineMode = value; final test = Invoker.current?.liveTest; if (test != null) { - timeline.mode = value; + throw StateError(''' +Cannot change global timeline mode within a test. +Use "timeline.mode" instead. +Example: timeline.mode = $value; + '''); } + _globalTimelineMode = value; } /// Use --dart-define=SPOT_TIMELINE_MODE=live|record|off to set the [TimlineMode] diff --git a/test/timeline/tap/local/local_timeline_tap_test.dart b/test/timeline/tap/local/local_timeline_tap_test.dart index 26e0dd32..0a1526e0 100644 --- a/test/timeline/tap/local/local_timeline_tap_test.dart +++ b/test/timeline/tap/local/local_timeline_tap_test.dart @@ -1,4 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/src/timeline/timeline.dart'; import '../timeline_tap_test_bodies.dart' as body; void main() { @@ -29,4 +30,11 @@ void main() { await body.liveWithErrorNoDuplicatesPrintsHtml(); }); }); + + test('Throws when global mode is changed during test', () async { + await body.throwOnGlobalTimelineChange( + initialGlobalMode: TimelineMode.live, + globalTimelineModeToSwitch: TimelineMode.record, + ); + }); } diff --git a/test/timeline/tap/timeline_tap_test_bodies.dart b/test/timeline/tap/timeline_tap_test_bodies.dart index 9ff2dc7c..1959bce8 100644 --- a/test/timeline/tap/timeline_tap_test_bodies.dart +++ b/test/timeline/tap/timeline_tap_test_bodies.dart @@ -45,6 +45,27 @@ Future recordWithoutError({ _testTimeLineContent(output: output, eventCount: 0); } +Future throwOnGlobalTimelineChange({ + bool isGlobalMode = false, + required TimelineMode initialGlobalMode, + required TimelineMode globalTimelineModeToSwitch, +}) async { + final stdout = await _outputFromTapTestProcess( + title: 'Throws if global timeline mode is changed mid test', + timelineMode: initialGlobalMode, + shouldFail: true, + isGlobalMode: isGlobalMode, + captureStart: 'The following StateError was thrown running a test:', + globalTimelineModeToSwitch: globalTimelineModeToSwitch, + ); + final expectedErrorMessage = ''' +Cannot change global timeline mode within a test. +Use "timeline.mode" instead. +Example: timeline.mode = $globalTimelineModeToSwitch; +'''; + expect(stdout, contains(expectedErrorMessage)); +} + Future recordWithError({ bool isGlobalMode = false, }) async { @@ -331,7 +352,13 @@ String _tapTestAsString({ required TimelineMode timelineMode, bool shouldFail = false, bool isGlobalMode = false, + TimelineMode? globalTimelineModeToSwitch, }) { + final switchPart = globalTimelineModeToSwitch != null + ? ''' + globalTimelineMode = TimelineMode.${globalTimelineModeToSwitch.toString().split('.').last}; + ''' + : ''; final testTitle = '${isGlobalMode ? 'Global: ' : 'Local: '}$title'; final globalInitiator = @@ -352,6 +379,7 @@ void main() async { final addButtonSelector = spotIcon(Icons.add); final subtractButtonSelector = spotIcon(Icons.remove); testWidgets("$testTitle", (WidgetTester tester) async { + $switchPart $localInitiator await tester.pumpWidget(const TimelineTestWidget()); addButtonSelector.existsOnce(); @@ -372,15 +400,18 @@ Future _outputFromTapTestProcess({ String captureStart = shared.timelineHeader, bool shouldFail = false, bool isGlobalMode = false, + TimelineMode? globalTimelineModeToSwitch, }) async { + final testAsString = _tapTestAsString( + title: title, + timelineMode: timelineMode, + shouldFail: shouldFail, + isGlobalMode: isGlobalMode, + globalTimelineModeToSwitch: globalTimelineModeToSwitch, + ); return process.runTestInProcessAndCaptureOutPut( shouldFail: shouldFail, - testAsString: _tapTestAsString( - title: title, - timelineMode: timelineMode, - shouldFail: shouldFail, - isGlobalMode: isGlobalMode, - ), + testAsString: testAsString, captureStart: captureStart, ); } From 6edfb34b8f98f5bf4f5ead9d8d60f026f47f368e Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Thu, 27 Jun 2024 04:24:55 +0200 Subject: [PATCH 103/119] Add test "Global timeline. Drag: record, without error" --- .../global_live_timeline_drag_test.dart | 11 ++ test/timeline/drag/timeline_drag_test.dart | 28 ++--- .../drag/timeline_drag_test_bodies.dart | 102 ++++++++++++++++++ 3 files changed, 122 insertions(+), 19 deletions(-) create mode 100644 test/timeline/drag/global/global_live_timeline_drag_test.dart create mode 100644 test/timeline/drag/timeline_drag_test_bodies.dart diff --git a/test/timeline/drag/global/global_live_timeline_drag_test.dart b/test/timeline/drag/global/global_live_timeline_drag_test.dart new file mode 100644 index 00000000..546286fa --- /dev/null +++ b/test/timeline/drag/global/global_live_timeline_drag_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/src/timeline/timeline.dart'; + +import '../timeline_drag_test_bodies.dart'; + +void main() { + globalTimelineMode = TimelineMode.live; + testWidgets('Global timeline. Drag: record, without error', (tester) async { + await liveTimelineWithoutErrorsDrag(tester, isGlobal: true); + }); +} diff --git a/test/timeline/drag/timeline_drag_test.dart b/test/timeline/drag/timeline_drag_test.dart index bdc7fddf..47498111 100644 --- a/test/timeline/drag/timeline_drag_test.dart +++ b/test/timeline/drag/timeline_drag_test.dart @@ -7,6 +7,7 @@ import 'package:test_process/test_process.dart'; import '../../util/capture_console_output.dart'; import '../timeline_test_shared.dart' as shared; import 'drag_until_visible_test_widget.dart'; +import 'timeline_drag_test_bodies.dart'; final _firstItemSelector = spotText('Item at index: 3', exact: true); final _secondItemSelector = spotText('Item at index: 27', exact: true); @@ -66,22 +67,11 @@ Future _testBody(WidgetTester tester) async { void main() { group('Drag Timeline Test', () { group('Without error', () { - testWidgets('Drag Until Visible - Live timeline', (tester) async { - final output = await captureConsoleOutput(() async { - timeline.mode = TimelineMode.live; - await _testBody(tester); - // Notify that the timeline of this type is already recording. - timeline.mode = TimelineMode.live; - }); - expect(output, contains('馃敶 - Now recording live timeline')); - _testTimeLineContent( - output: output, - totalExpectedOffset: _passingOffset, - drags: _passingDragAmount, - ); - expect(output, contains('馃敶 - Already recording live timeline')); + testWidgets('Local timeline. Drag: record, without error', + (tester) async { + await liveTimelineWithoutErrorsDrag(tester); }); - testWidgets('Start with Timeline Mode off', (tester) async { + testWidgets('Local: off', (tester) async { final output = await captureConsoleOutput(() async { timeline.mode = TimelineMode.off; await _testBody(tester); @@ -92,7 +82,7 @@ void main() { expect(splitted.length, 1); expect(splitted.first, expectedOutput); }); - testWidgets('Turn timeline mode off during test', (tester) async { + testWidgets('Local: record, turn off during test', (tester) async { final output = await captureConsoleOutput(() async { timeline.mode = TimelineMode.record; await _testBody(tester); @@ -111,7 +101,7 @@ void main() { expect(output, contains('鈴革笌 - Timeline recording stopped')); expect(output, contains('鈴革笌 - Timeline recording is off')); }); - testWidgets('act.drag: OnError timeline - without error', (tester) async { + testWidgets('Local: record - without error', (tester) async { final output = await captureConsoleOutput(() async { timeline.mode = TimelineMode.record; await _testBody(tester); @@ -127,7 +117,7 @@ void main() { }); }); group('Teardown test', () { - test('OnError timeline - with error, prints timeline and html', () async { + test('Local: record, with error, prints timeline and html', () async { final tempDir = Directory.systemTemp.createTempSync(); final tempTestFile = File('${tempDir.path}/temp_test.dart'); @@ -182,7 +172,7 @@ void main() { isTrue, ); }); - test('Live timeline - without error, prints HTML', () async { + test('Local: live - without error, prints HTML', () async { final tempDir = Directory.systemTemp.createTempSync(); final tempTestFile = File('${tempDir.path}/temp_test.dart'); await tempTestFile.writeAsString( diff --git a/test/timeline/drag/timeline_drag_test_bodies.dart b/test/timeline/drag/timeline_drag_test_bodies.dart new file mode 100644 index 00000000..5e93cb72 --- /dev/null +++ b/test/timeline/drag/timeline_drag_test_bodies.dart @@ -0,0 +1,102 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; + +import '../../util/capture_console_output.dart'; +import '../timeline_test_shared.dart'; +import 'drag_until_visible_test_widget.dart'; + +final _firstItemSelector = spotText('Item at index: 3', exact: true); +final _secondItemSelector = spotText('Item at index: 27', exact: true); + +const _failingDragAmount = 10; +const _failingOffset = Offset(0, -1000); +const _passingDragAmount = 23; +const _passingOffset = Offset(0, -2300); + +Future liveTimelineWithoutErrorsDrag( + WidgetTester tester, { + bool isGlobal = false, +}) async { + final output = await captureConsoleOutput(() async { + if (!isGlobal) { + timeline.mode = TimelineMode.live; + } + await _testBody(tester); + }); + final hasMessage = output.contains('馃敶 - Recording live timeline'); + + expect(hasMessage, isGlobal ? isFalse : isTrue); + _testTimeLineContent( + output: output, + totalExpectedOffset: _passingOffset, + drags: _passingDragAmount, + ); +} + +Future _testBody(WidgetTester tester) async { + await tester.pumpWidget(const DragUntilVisibleTestWidget()); + _firstItemSelector.existsOnce(); + _secondItemSelector.doesNotExist(); + await act.dragUntilVisible( + dragStart: _firstItemSelector, + dragTarget: _secondItemSelector, + moveStep: const Offset(0, -100), + maxIteration: _passingDragAmount, + ); + _secondItemSelector.existsOnce(); +} + +void _testTimeLineContent({ + required String output, + required Offset totalExpectedOffset, + required int drags, + bool runInTestProcess = false, +}) { + final isGoingDown = totalExpectedOffset.dy < 0; + final findsWidget = drags == _passingDragAmount; + final eventLines = + output.split('\n').where((line) => line.startsWith('Event:')); + final startEvent = _replaceOffsetWithDxDy(eventLines.first); + final endEvent = + 'Event: Target ${_secondItemSelector.toStringBreadcrumb()} ${findsWidget ? 'found' : 'not found'} after $drags drags. Total dragged offset: $totalExpectedOffset'; + + for (int i = 0; i < 2; i++) { + expect( + RegExp(timelineHeader).allMatches(output).length, + 2, + ); + if (i == 0) { + expect( + startEvent, + 'Event: Scrolling ${isGoingDown ? 'downwards' : 'upwards'} from Offset(dx,dy) in order to find ${_secondItemSelector.toStringBreadcrumb()}.', + ); + } else { + expect( + eventLines.last, + endEvent, + ); + } + expect( + RegExp('Caller: at ${runInTestProcess ? 'main' : '_testBody'}') + .allMatches(output) + .length, + 2, + ); + expect( + RegExp('Screenshot: file:').allMatches(output).length, + 2, + ); + expect( + RegExp('Timestamp: ').allMatches(output).length, + 2, + ); + } +} + +String _replaceOffsetWithDxDy(String originalString) { + // Regular expression to match 'Offset(any values)' + final RegExp offsetPattern = RegExp(r'Offset\([^)]*\)'); + + // Replace all matches with 'Offset(dx,dy)' + return originalString.replaceAll(offsetPattern, 'Offset(dx,dy)'); +} From 65e0692f19e2c2286d4e4c16bfb0b2bbc133380b Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Thu, 27 Jun 2024 12:15:45 +0200 Subject: [PATCH 104/119] Add test "Global: off does not record" --- .../global/global_live_timeline_drag_test.dart | 2 +- .../global/global_off_timeline_drag_test.dart | 11 +++++++++++ test/timeline/drag/timeline_drag_test.dart | 15 +++------------ .../drag/timeline_drag_test_bodies.dart | 17 +++++++++++++++++ 4 files changed, 32 insertions(+), 13 deletions(-) create mode 100644 test/timeline/drag/global/global_off_timeline_drag_test.dart diff --git a/test/timeline/drag/global/global_live_timeline_drag_test.dart b/test/timeline/drag/global/global_live_timeline_drag_test.dart index 546286fa..13c53dcf 100644 --- a/test/timeline/drag/global/global_live_timeline_drag_test.dart +++ b/test/timeline/drag/global/global_live_timeline_drag_test.dart @@ -5,7 +5,7 @@ import '../timeline_drag_test_bodies.dart'; void main() { globalTimelineMode = TimelineMode.live; - testWidgets('Global timeline. Drag: record, without error', (tester) async { + testWidgets('Global: record, without error', (tester) async { await liveTimelineWithoutErrorsDrag(tester, isGlobal: true); }); } diff --git a/test/timeline/drag/global/global_off_timeline_drag_test.dart b/test/timeline/drag/global/global_off_timeline_drag_test.dart new file mode 100644 index 00000000..6b952dd6 --- /dev/null +++ b/test/timeline/drag/global/global_off_timeline_drag_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/src/timeline/timeline.dart'; + +import '../timeline_drag_test_bodies.dart'; + +void main() { + globalTimelineMode = TimelineMode.off; + testWidgets('Global: off does not record', (tester) async { + await offTimelineWithoutErrorsDrag(tester, isGlobal: true); + }); +} diff --git a/test/timeline/drag/timeline_drag_test.dart b/test/timeline/drag/timeline_drag_test.dart index 47498111..b1019067 100644 --- a/test/timeline/drag/timeline_drag_test.dart +++ b/test/timeline/drag/timeline_drag_test.dart @@ -67,20 +67,11 @@ Future _testBody(WidgetTester tester) async { void main() { group('Drag Timeline Test', () { group('Without error', () { - testWidgets('Local timeline. Drag: record, without error', - (tester) async { + testWidgets('Local: record, without error', (tester) async { await liveTimelineWithoutErrorsDrag(tester); }); - testWidgets('Local: off', (tester) async { - final output = await captureConsoleOutput(() async { - timeline.mode = TimelineMode.off; - await _testBody(tester); - }); - final splitted = output.split('\n') - ..removeWhere((line) => line.isEmpty); - const expectedOutput = '鈴革笌 - Timeline recording is off'; - expect(splitted.length, 1); - expect(splitted.first, expectedOutput); + testWidgets('Local: off does not record', (tester) async { + await offTimelineWithoutErrorsDrag(tester); }); testWidgets('Local: record, turn off during test', (tester) async { final output = await captureConsoleOutput(() async { diff --git a/test/timeline/drag/timeline_drag_test_bodies.dart b/test/timeline/drag/timeline_drag_test_bodies.dart index 5e93cb72..7b7f02e7 100644 --- a/test/timeline/drag/timeline_drag_test_bodies.dart +++ b/test/timeline/drag/timeline_drag_test_bodies.dart @@ -33,6 +33,23 @@ Future liveTimelineWithoutErrorsDrag( ); } +Future offTimelineWithoutErrorsDrag( + WidgetTester tester, { + bool isGlobal = false, +}) async { + final output = await captureConsoleOutput(() async { + if (!isGlobal) { + timeline.mode = TimelineMode.off; + } + await _testBody(tester); + }); + final lines = output.split('\n')..removeWhere((line) => line.isEmpty); + const expectedOutput = '鈴革笌 - Timeline recording is off'; + final hasMessage = output.contains(expectedOutput); + expect(hasMessage, isGlobal ? isFalse : isTrue); + expect(lines.length, isGlobal ? 0 : 1); +} + Future _testBody(WidgetTester tester) async { await tester.pumpWidget(const DragUntilVisibleTestWidget()); _firstItemSelector.existsOnce(); From e2c936bd8b11c22171f4003195d5880bda954e50 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Thu, 27 Jun 2024 12:58:32 +0200 Subject: [PATCH 105/119] Add test "Global: record, turn off during test" --- .../global_record_timeline_drag_test.dart | 11 +++++++++++ test/timeline/drag/timeline_drag_test.dart | 18 +----------------- .../drag/timeline_drag_test_bodies.dart | 13 +++++++++++++ 3 files changed, 25 insertions(+), 17 deletions(-) create mode 100644 test/timeline/drag/global/global_record_timeline_drag_test.dart diff --git a/test/timeline/drag/global/global_record_timeline_drag_test.dart b/test/timeline/drag/global/global_record_timeline_drag_test.dart new file mode 100644 index 00000000..fec1664d --- /dev/null +++ b/test/timeline/drag/global/global_record_timeline_drag_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/src/timeline/timeline.dart'; + +import '../timeline_drag_test_bodies.dart'; + +void main() { + globalTimelineMode = TimelineMode.record; + testWidgets('Global: record, turn off during test', (tester) async { + await recordTurnOffDuringTestDrag(tester, isGlobal: true); + }); +} diff --git a/test/timeline/drag/timeline_drag_test.dart b/test/timeline/drag/timeline_drag_test.dart index b1019067..c51bd94e 100644 --- a/test/timeline/drag/timeline_drag_test.dart +++ b/test/timeline/drag/timeline_drag_test.dart @@ -74,23 +74,7 @@ void main() { await offTimelineWithoutErrorsDrag(tester); }); testWidgets('Local: record, turn off during test', (tester) async { - final output = await captureConsoleOutput(() async { - timeline.mode = TimelineMode.record; - await _testBody(tester); - // Notify that the recording is off - timeline.mode = TimelineMode.off; - timeline.mode = TimelineMode.off; - }); - expect(output, contains('馃敶 - Now recording live timeline')); - - _testTimeLineContent( - output: output, - totalExpectedOffset: _passingOffset, - drags: _passingDragAmount, - ); - - expect(output, contains('鈴革笌 - Timeline recording stopped')); - expect(output, contains('鈴革笌 - Timeline recording is off')); + await recordTurnOffDuringTestDrag(tester); }); testWidgets('Local: record - without error', (tester) async { final output = await captureConsoleOutput(() async { diff --git a/test/timeline/drag/timeline_drag_test_bodies.dart b/test/timeline/drag/timeline_drag_test_bodies.dart index 7b7f02e7..586153d2 100644 --- a/test/timeline/drag/timeline_drag_test_bodies.dart +++ b/test/timeline/drag/timeline_drag_test_bodies.dart @@ -50,6 +50,19 @@ Future offTimelineWithoutErrorsDrag( expect(lines.length, isGlobal ? 0 : 1); } +Future recordTurnOffDuringTestDrag( + WidgetTester tester, { + bool isGlobal = false, +}) async { + final output = await captureConsoleOutput(() async { + timeline.mode = TimelineMode.off; + await _testBody(tester); + }); + final lines = output.split('\n')..removeWhere((line) => line.isEmpty); + expect(lines.length, 1); + expect(lines.first, contains('鈴革笌 - Timeline recording is off')); +} + Future _testBody(WidgetTester tester) async { await tester.pumpWidget(const DragUntilVisibleTestWidget()); _firstItemSelector.existsOnce(); From 22039dae22c6d4eeb16b9ffc2254f7ca83587c67 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Thu, 27 Jun 2024 13:10:15 +0200 Subject: [PATCH 106/119] Add test "Global: record - without error" --- .../global_record_timeline_drag_test.dart | 3 +++ test/timeline/drag/timeline_drag_test.dart | 13 +------------ .../drag/timeline_drag_test_bodies.dart | 17 +++++++++++++++++ 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/test/timeline/drag/global/global_record_timeline_drag_test.dart b/test/timeline/drag/global/global_record_timeline_drag_test.dart index fec1664d..002d3ba0 100644 --- a/test/timeline/drag/global/global_record_timeline_drag_test.dart +++ b/test/timeline/drag/global/global_record_timeline_drag_test.dart @@ -8,4 +8,7 @@ void main() { testWidgets('Global: record, turn off during test', (tester) async { await recordTurnOffDuringTestDrag(tester, isGlobal: true); }); + testWidgets('Global: record - without error', (tester) async { + await recordTurnOffDuringTestDrag(tester, isGlobal: true); + }); } diff --git a/test/timeline/drag/timeline_drag_test.dart b/test/timeline/drag/timeline_drag_test.dart index c51bd94e..49eecc36 100644 --- a/test/timeline/drag/timeline_drag_test.dart +++ b/test/timeline/drag/timeline_drag_test.dart @@ -77,18 +77,7 @@ void main() { await recordTurnOffDuringTestDrag(tester); }); testWidgets('Local: record - without error', (tester) async { - final output = await captureConsoleOutput(() async { - timeline.mode = TimelineMode.record; - await _testBody(tester); - timeline.mode = TimelineMode.record; - }); - final lines = output.split('\n')..removeWhere((line) => line.isEmpty); - expect(lines.first, '馃敶 - Now recording error output timeline'); - expect(lines.second, '馃敶 - Already recording error output timeline'); - - // Neither timeline output nor HTML link when onError timeline is - // recorded and no error occurs. - expect(lines.length, 2); + await recordNoErrorsDrag(tester); }); }); group('Teardown test', () { diff --git a/test/timeline/drag/timeline_drag_test_bodies.dart b/test/timeline/drag/timeline_drag_test_bodies.dart index 586153d2..19d2ce94 100644 --- a/test/timeline/drag/timeline_drag_test_bodies.dart +++ b/test/timeline/drag/timeline_drag_test_bodies.dart @@ -1,3 +1,4 @@ +import 'package:dartx/dartx.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; @@ -63,6 +64,22 @@ Future recordTurnOffDuringTestDrag( expect(lines.first, contains('鈴革笌 - Timeline recording is off')); } +Future recordNoErrorsDrag( + WidgetTester tester, { + bool isGlobal = false, +}) async { + final output = await captureConsoleOutput(() async { + // Won't change anything, since it's default. Here to make sure + // nothing is printed when the mode doesn't change. + timeline.mode = TimelineMode.record; + await _testBody(tester); + }); + final lines = output.split('\n')..removeWhere((line) => line.isEmpty); + // Neither timeline output nor HTML link when onError timeline is + // recorded when no error occurs. + expect(lines.length, 0); +} + Future _testBody(WidgetTester tester) async { await tester.pumpWidget(const DragUntilVisibleTestWidget()); _firstItemSelector.existsOnce(); From 75b1891ef92daff061459064627b93ec90ce92f1 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Thu, 27 Jun 2024 13:36:19 +0200 Subject: [PATCH 107/119] Add test "Global: record, with error, prints timeline and html" --- .../global_record_timeline_drag_test.dart | 3 + test/timeline/drag/timeline_drag_test.dart | 71 +----------- .../drag/timeline_drag_test_bodies.dart | 109 +++++++++++++++++- 3 files changed, 109 insertions(+), 74 deletions(-) diff --git a/test/timeline/drag/global/global_record_timeline_drag_test.dart b/test/timeline/drag/global/global_record_timeline_drag_test.dart index 002d3ba0..e4a1ddf5 100644 --- a/test/timeline/drag/global/global_record_timeline_drag_test.dart +++ b/test/timeline/drag/global/global_record_timeline_drag_test.dart @@ -11,4 +11,7 @@ void main() { testWidgets('Global: record - without error', (tester) async { await recordTurnOffDuringTestDrag(tester, isGlobal: true); }); + test('Global: record, with error, prints timeline and html', () async { + await recordWithErrorPrintsHTML(isGlobal: true); + }); } diff --git a/test/timeline/drag/timeline_drag_test.dart b/test/timeline/drag/timeline_drag_test.dart index 49eecc36..767c1c9b 100644 --- a/test/timeline/drag/timeline_drag_test.dart +++ b/test/timeline/drag/timeline_drag_test.dart @@ -9,11 +9,7 @@ import '../timeline_test_shared.dart' as shared; import 'drag_until_visible_test_widget.dart'; import 'timeline_drag_test_bodies.dart'; -final _firstItemSelector = spotText('Item at index: 3', exact: true); final _secondItemSelector = spotText('Item at index: 27', exact: true); - -const _failingDragAmount = 10; -const _failingOffset = Offset(0, -1000); const _passingDragAmount = 23; const _passingOffset = Offset(0, -2300); @@ -51,19 +47,6 @@ void main() async { '''; } -Future _testBody(WidgetTester tester) async { - await tester.pumpWidget(const DragUntilVisibleTestWidget()); - _firstItemSelector.existsOnce(); - _secondItemSelector.doesNotExist(); - await act.dragUntilVisible( - dragStart: _firstItemSelector, - dragTarget: _secondItemSelector, - moveStep: const Offset(0, -100), - maxIteration: _passingDragAmount, - ); - _secondItemSelector.existsOnce(); -} - void main() { group('Drag Timeline Test', () { group('Without error', () { @@ -82,59 +65,7 @@ void main() { }); group('Teardown test', () { test('Local: record, with error, prints timeline and html', () async { - final tempDir = Directory.systemTemp.createTempSync(); - final tempTestFile = File('${tempDir.path}/temp_test.dart'); - - await tempTestFile.writeAsString( - _testAsString( - title: 'OnError timeline - with error, prints timeline', - timelineMode: TimelineMode.record, - drags: _failingDragAmount, - ), - ); - final testProcess = - await TestProcess.start('flutter', ['test', tempTestFile.path]); - final stdoutBuffer = StringBuffer(); - bool write = false; - - await for (final line in testProcess.stdoutStream()) { - if (line.isEmpty) continue; - - if (!write) { - if (line == 'Timeline') { - write = true; - } - } - - if (write) { - stdoutBuffer.writeln(line); - } - } - // Error happens - await testProcess.shouldExit(1); - - if (tempDir.existsSync()) { - tempDir.deleteSync(recursive: true); - } - - final stdout = stdoutBuffer.toString(); - _testTimeLineContent( - output: stdout, - drags: _failingDragAmount, - totalExpectedOffset: _failingOffset, - runInTestProcess: true, - ); - - final htmlLine = stdout - .split('\n') - .firstWhere((line) => line.startsWith('View time line here:')); - - expect( - htmlLine.endsWith( - 'timeline-onerror-timeline-with-error-prints-timeline.html', - ), - isTrue, - ); + await recordWithErrorPrintsHTML(); }); test('Local: live - without error, prints HTML', () async { final tempDir = Directory.systemTemp.createTempSync(); diff --git a/test/timeline/drag/timeline_drag_test_bodies.dart b/test/timeline/drag/timeline_drag_test_bodies.dart index 19d2ce94..1e565029 100644 --- a/test/timeline/drag/timeline_drag_test_bodies.dart +++ b/test/timeline/drag/timeline_drag_test_bodies.dart @@ -1,8 +1,12 @@ +import 'dart:io'; + import 'package:dartx/dartx.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import '../../util/capture_console_output.dart'; +import '../../util/run_test_in_process.dart' as process; +import '../timeline_test_shared.dart' as shared; import '../timeline_test_shared.dart'; import 'drag_until_visible_test_widget.dart'; @@ -80,6 +84,34 @@ Future recordNoErrorsDrag( expect(lines.length, 0); } +Future recordWithErrorPrintsHTML({ + bool isGlobal = false, +}) async { + final stdout = await _outputFromDragTestProcess( + title: 'OnError timeline - with error, prints timeline', + timelineMode: TimelineMode.record, + drags: _failingDragAmount, + isGlobalMode: isGlobal, + ); + + _testTimeLineContent( + output: stdout, + drags: _failingDragAmount, + totalExpectedOffset: _failingOffset, + ); + + final htmlLine = stdout + .split('\n') + .firstWhere((line) => line.startsWith('View time line here:')); + final prefix = isGlobal ? 'global' : 'local'; + expect( + htmlLine.endsWith( + 'timeline-$prefix-onerror-timeline-with-error-prints-timeline.html', + ), + isTrue, + ); +} + Future _testBody(WidgetTester tester) async { await tester.pumpWidget(const DragUntilVisibleTestWidget()); _firstItemSelector.existsOnce(); @@ -97,7 +129,6 @@ void _testTimeLineContent({ required String output, required Offset totalExpectedOffset, required int drags, - bool runInTestProcess = false, }) { final isGoingDown = totalExpectedOffset.dy < 0; final findsWidget = drags == _passingDragAmount; @@ -124,9 +155,7 @@ void _testTimeLineContent({ ); } expect( - RegExp('Caller: at ${runInTestProcess ? 'main' : '_testBody'}') - .allMatches(output) - .length, + RegExp('Caller: at').allMatches(output).length, 2, ); expect( @@ -147,3 +176,75 @@ String _replaceOffsetWithDxDy(String originalString) { // Replace all matches with 'Offset(dx,dy)' return originalString.replaceAll(offsetPattern, 'Offset(dx,dy)'); } + +Future _outputFromDragTestProcess({ + required String title, + required TimelineMode timelineMode, + String captureStart = shared.timelineHeader, + bool isGlobalMode = false, + TimelineMode? globalTimelineModeToSwitch, + required int drags, +}) async { + final testAsString = _testAsString( + title: title, + timelineMode: timelineMode, + isGlobalMode: isGlobalMode, + globalTimelineModeToSwitch: globalTimelineModeToSwitch, + drags: drags, + ); + return process.runTestInProcessAndCaptureOutPut( + shouldFail: + drags == _failingDragAmount || globalTimelineModeToSwitch != null, + testAsString: testAsString, + captureStart: captureStart, + ); +} + +String _testAsString({ + required String title, + required TimelineMode timelineMode, + required int drags, + bool isGlobalMode = false, + TimelineMode? globalTimelineModeToSwitch, +}) { + final switchPart = globalTimelineModeToSwitch != null + ? ''' + globalTimelineMode = TimelineMode.${globalTimelineModeToSwitch.toString().split('.').last}; + ''' + : ''; + final testTitle = '${isGlobalMode ? 'Global: ' : 'Local: '}$title'; + + final globalInitiator = + isGlobalMode ? shared.globalTimelineInitiator(timelineMode) : ''; + + final localInitiator = + isGlobalMode ? '' : shared.localTimelineInitiator(timelineMode); + + final widgetPart = + File('test/timeline/drag/drag_until_visible_test_widget.dart') + .readAsStringSync(); + return ''' +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; +import 'package:spot/src/timeline/timeline.dart';\n +$widgetPart\n +void main() async { +$globalInitiator + testWidgets("$testTitle", (WidgetTester tester) async { + $localInitiator + $switchPart + await tester.pumpWidget(const DragUntilVisibleTestWidget()); + final firstItem = spotText('Item at index: 3', exact: true)..existsOnce(); + final secondItem = spotText('Item at index: 27', exact: true) + ..doesNotExist(); + await act.dragUntilVisible( + dragStart: firstItem, + dragTarget: secondItem, + maxIteration: $drags, + moveStep: const Offset(0, -100), + ); + secondItem.existsOnce(); + }); +} +'''; +} From e83db5bd002af0bdcbf7b971e3fa4a8e38efcf20 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 28 Jun 2024 11:47:46 +0200 Subject: [PATCH 108/119] Add test "Global: record, without error" --- .../global_live_timeline_drag_test.dart | 3 + test/timeline/drag/timeline_drag_test.dart | 57 +------------------ .../drag/timeline_drag_test_bodies.dart | 31 ++++++++++ 3 files changed, 35 insertions(+), 56 deletions(-) diff --git a/test/timeline/drag/global/global_live_timeline_drag_test.dart b/test/timeline/drag/global/global_live_timeline_drag_test.dart index 13c53dcf..86717937 100644 --- a/test/timeline/drag/global/global_live_timeline_drag_test.dart +++ b/test/timeline/drag/global/global_live_timeline_drag_test.dart @@ -8,4 +8,7 @@ void main() { testWidgets('Global: record, without error', (tester) async { await liveTimelineWithoutErrorsDrag(tester, isGlobal: true); }); + testWidgets('Global: record, without error', (tester) async { + await liveTimelineWithoutErrorsDrag(tester, isGlobal: true); + }); } diff --git a/test/timeline/drag/timeline_drag_test.dart b/test/timeline/drag/timeline_drag_test.dart index 6bb24795..ec7d2d9f 100644 --- a/test/timeline/drag/timeline_drag_test.dart +++ b/test/timeline/drag/timeline_drag_test.dart @@ -65,62 +65,7 @@ void main() { await recordWithErrorPrintsHTML(); }); test('Local: live - without error, prints HTML', () async { - final tempDir = Directory.systemTemp.createTempSync(); - final tempTestFile = File('${tempDir.path}/temp_test.dart'); - await tempTestFile.writeAsString( - _testAsString( - title: 'Live timeline - without error, prints HTML', - timelineMode: TimelineMode.live, - drags: _passingDragAmount, - ), - ); - final testProcess = - await TestProcess.start('flutter', ['test', tempTestFile.path]); - final stdoutBuffer = StringBuffer(); - bool write = false; - - await for (final line in testProcess.stdoutStream()) { - if (line.isEmpty) continue; - - if (!write) { - if (line == 'Timeline' || line == _header) { - write = true; - } - } - - if (write) { - stdoutBuffer.writeln(line); - } - } - - await testProcess.shouldExit(0); - - if (tempDir.existsSync()) { - tempDir.deleteSync(recursive: true); - } - - final stdout = stdoutBuffer.toString(); - - // Does not start with 'Timeline', this only happens on error - expect(stdout.startsWith('Timeline'), isFalse); - - _testTimeLineContent( - output: stdout, - drags: _passingDragAmount, - totalExpectedOffset: _passingOffset, - runInTestProcess: true, - ); - - final htmlLine = stdout - .split('\n') - .firstWhere((line) => line.startsWith('View time line here:')); - - expect( - htmlLine.endsWith( - 'timeline-live-timeline-without-error-prints-html.html', - ), - isTrue, - ); + await liveWithoutErrorPrintsHTML(); }); // test('Live timeline - with error, no duplicates, prints HTML', () async { // final tempDir = Directory.systemTemp.createTempSync(); diff --git a/test/timeline/drag/timeline_drag_test_bodies.dart b/test/timeline/drag/timeline_drag_test_bodies.dart index f42fb242..684a602f 100644 --- a/test/timeline/drag/timeline_drag_test_bodies.dart +++ b/test/timeline/drag/timeline_drag_test_bodies.dart @@ -83,6 +83,37 @@ Future recordNoErrorsDrag( expect(lines.length, 0); } +Future liveWithoutErrorPrintsHTML({ + bool isGlobal = false, +}) async { + final stdout = await _outputFromDragTestProcess( + title: 'Live timeline - without error, prints HTML', + timelineMode: TimelineMode.live, + drags: _passingDragAmount, + ); + + // Does not start with 'Timeline', this only happens on error + expect(stdout.startsWith('Timeline'), isFalse); + + _testTimeLineContent( + output: stdout, + drags: _passingDragAmount, + totalExpectedOffset: _passingOffset, + ); + + final htmlLine = stdout + .split('\n') + .firstWhere((line) => line.startsWith('View time line here:')); + + final prefix = isGlobal ? 'global' : 'local'; + expect( + htmlLine.endsWith( + 'timeline-$prefix-live-timeline-without-error-prints-html.html', + ), + isTrue, + ); +} + Future recordWithErrorPrintsHTML({ bool isGlobal = false, }) async { From 3d8e644cadc3030d37a8beff4c1b3453d88e46c4 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Fri, 28 Jun 2024 15:57:00 +0200 Subject: [PATCH 109/119] Add test "Global: live - with error, prints HTML, no duplicates" --- .../global_live_timeline_drag_test.dart | 5 ++- test/timeline/drag/timeline_drag_test.dart | 3 ++ .../drag/timeline_drag_test_bodies.dart | 45 ++++++++++++++++++- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/test/timeline/drag/global/global_live_timeline_drag_test.dart b/test/timeline/drag/global/global_live_timeline_drag_test.dart index 86717937..a484f881 100644 --- a/test/timeline/drag/global/global_live_timeline_drag_test.dart +++ b/test/timeline/drag/global/global_live_timeline_drag_test.dart @@ -5,10 +5,13 @@ import '../timeline_drag_test_bodies.dart'; void main() { globalTimelineMode = TimelineMode.live; - testWidgets('Global: record, without error', (tester) async { + testWidgets('Global: live, without error', (tester) async { await liveTimelineWithoutErrorsDrag(tester, isGlobal: true); }); testWidgets('Global: record, without error', (tester) async { await liveTimelineWithoutErrorsDrag(tester, isGlobal: true); }); + test('Global: live - with error, prints HTML, no duplicates', () async { + await liveWitErrorPrintsHTMLNoDuplicates(isGlobal: true); + }); } diff --git a/test/timeline/drag/timeline_drag_test.dart b/test/timeline/drag/timeline_drag_test.dart index ec7d2d9f..57b5e09d 100644 --- a/test/timeline/drag/timeline_drag_test.dart +++ b/test/timeline/drag/timeline_drag_test.dart @@ -67,6 +67,9 @@ void main() { test('Local: live - without error, prints HTML', () async { await liveWithoutErrorPrintsHTML(); }); + test('Local: live - with error, prints HTML, no duplicates', () async { + await liveWitErrorPrintsHTMLNoDuplicates(); + }); // test('Live timeline - with error, no duplicates, prints HTML', () async { // final tempDir = Directory.systemTemp.createTempSync(); // final tempTestFile = File('${tempDir.path}/temp_test.dart'); diff --git a/test/timeline/drag/timeline_drag_test_bodies.dart b/test/timeline/drag/timeline_drag_test_bodies.dart index 684a602f..d80b7579 100644 --- a/test/timeline/drag/timeline_drag_test_bodies.dart +++ b/test/timeline/drag/timeline_drag_test_bodies.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:dartx/dartx.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; @@ -114,6 +115,46 @@ Future liveWithoutErrorPrintsHTML({ ); } +Future liveWitErrorPrintsHTMLNoDuplicates({ + bool isGlobal = false, +}) async { + final stdout = await _outputFromDragTestProcess( + isGlobalMode: isGlobal, + title: 'Live timeline - with error, no duplicates, prints HTML', + timelineMode: TimelineMode.live, + drags: _failingDragAmount, + captureStart: + isGlobal ? shared.timelineHeader : '馃敶 - Recording live timeline', + ); + + final lines = stdout.split('\n'); + // In the local test, the default timeline mode is 'record'. + // Switching to live within the test results in a change to the mode, + // which is documented in the output. + expect( + lines.first, + isGlobal ? shared.timelineHeader : '馃敶 - Recording live timeline', + ); + + _testTimeLineContent( + output: stdout, + drags: _failingDragAmount, + totalExpectedOffset: _failingOffset, + ); + + final htmlLine = stdout + .split('\n') + .firstWhere((line) => line.startsWith('View time line here:')); + + final prefix = isGlobal ? 'global' : 'local'; + expect( + htmlLine.endsWith( + 'timeline-$prefix-live-timeline-with-error-no-duplicates-prints-html.html', + ), + isTrue, + ); +} + Future recordWithErrorPrintsHTML({ bool isGlobal = false, }) async { @@ -222,6 +263,7 @@ Future _outputFromDragTestProcess({ globalTimelineModeToSwitch: globalTimelineModeToSwitch, drags: drags, ); + print('testAsString: $testAsString'); return process.runTestInProcessAndCaptureOutPut( shouldFail: drags == _failingDragAmount || globalTimelineModeToSwitch != null, @@ -247,8 +289,7 @@ String _testAsString({ final globalInitiator = isGlobalMode ? shared.globalTimelineInitiator(timelineMode) : ''; - final localInitiator = - isGlobalMode ? '' : shared.localTimelineInitiator(timelineMode); + final localInitiator = shared.localTimelineInitiator(timelineMode); final widgetPart = File('test/timeline/drag/drag_until_visible_test_widget.dart') From 7f78a836521fb534b83a503d00f3c63881dfd0dc Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 3 Jul 2024 02:10:36 +0200 Subject: [PATCH 110/119] Create test body classes --- .../drag/act_drag_timeline_test_bodies.dart | 361 +++++++++++++++ .../global_live_timeline_drag_test.dart | 17 +- .../global/global_off_timeline_drag_test.dart | 4 +- .../global_record_timeline_drag_test.dart | 8 +- .../drag/local/local_timeline_drag_test.dart | 32 ++ test/timeline/drag/timeline_drag_test.dart | 194 -------- .../drag/timeline_drag_test_bodies.dart | 321 -------------- .../tap/act_tap_timeline_test_bodies.dart | 418 ++++++++++++++++++ .../global/global_live_timeline_tap_test.dart | 10 +- .../global/global_off_timeline_tap_test.dart | 4 +- .../global_record_timeline_tap_test.dart | 6 +- .../tap/local/local_timeline_tap_test.dart | 18 +- .../tap/timeline_tap_test_bodies.dart | 417 ----------------- 13 files changed, 848 insertions(+), 962 deletions(-) create mode 100644 test/timeline/drag/act_drag_timeline_test_bodies.dart create mode 100644 test/timeline/drag/local/local_timeline_drag_test.dart delete mode 100644 test/timeline/drag/timeline_drag_test.dart delete mode 100644 test/timeline/drag/timeline_drag_test_bodies.dart create mode 100644 test/timeline/tap/act_tap_timeline_test_bodies.dart delete mode 100644 test/timeline/tap/timeline_tap_test_bodies.dart diff --git a/test/timeline/drag/act_drag_timeline_test_bodies.dart b/test/timeline/drag/act_drag_timeline_test_bodies.dart new file mode 100644 index 00000000..739294e2 --- /dev/null +++ b/test/timeline/drag/act_drag_timeline_test_bodies.dart @@ -0,0 +1,361 @@ +import 'dart:io'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; + +import '../../util/capture_console_output.dart'; +import '../../util/run_test_in_process.dart' as process; +import '../timeline_test_shared.dart' as shared; +import '../timeline_test_shared.dart'; +import 'drag_until_visible_test_widget.dart'; + +class ActDragTimelineTestBodies { + static final _firstItemSelector = spotText('Item at index: 3', exact: true); + static final _secondItemSelector = spotText('Item at index: 27', exact: true); + + static const _failingDragAmount = 10; + static const _failingOffset = Offset(0, -1000); + static const _passingDragAmount = 23; + static const _passingOffset = Offset(0, -2300); + + static Future liveWithoutError( + WidgetTester tester, { + bool isGlobal = false, + }) async { + final output = await captureConsoleOutput(() async { + if (!isGlobal) { + timeline.mode = TimelineMode.live; + } + await _testBody(tester); + }); + final hasMessage = output.contains('馃敶 - Recording live timeline'); + + expect(hasMessage, isGlobal ? isFalse : isTrue); + _testTimeLineContent( + output: output, + totalExpectedOffset: _passingOffset, + drags: _passingDragAmount, + ); + } + + static Future offWithoutError( + WidgetTester tester, { + bool isGlobal = false, + }) async { + final output = await captureConsoleOutput(() async { + if (!isGlobal) { + timeline.mode = TimelineMode.off; + } + await _testBody(tester); + }); + final lines = output.split('\n')..removeWhere((line) => line.isEmpty); + const expectedOutput = '鈴革笌 - Timeline recording is off'; + final hasMessage = output.contains(expectedOutput); + expect(hasMessage, isGlobal ? isFalse : isTrue); + expect(lines.length, isGlobal ? 0 : 1); + } + + static Future recordTurnOff( + WidgetTester tester, { + bool isGlobal = false, + }) async { + final output = await captureConsoleOutput(() async { + timeline.mode = TimelineMode.off; + await _testBody(tester); + }); + final lines = output.split('\n')..removeWhere((line) => line.isEmpty); + expect(lines.length, 1); + expect(lines.first, contains('鈴革笌 - Timeline recording is off')); + } + + static Future recordNoError( + WidgetTester tester, { + bool isGlobal = false, + }) async { + final output = await captureConsoleOutput(() async { + // Won't change anything, since it's default. Here to make sure + // nothing is printed when the mode doesn't change. + timeline.mode = TimelineMode.record; + await _testBody(tester); + }); + final lines = output.split('\n')..removeWhere((line) => line.isEmpty); + // Neither timeline output nor HTML link when onError timeline is + // recorded when no error occurs. + expect(lines.length, 0); + } + + static Future liveWithoutErrorPrintsHTML({ + bool isGlobal = false, + }) async { + final stdout = await _outputFromDragTestProcess( + title: 'Live timeline - without error, prints HTML', + timelineMode: TimelineMode.live, + drags: _passingDragAmount, + ); + + // Does not start with 'Timeline', this only happens on error + expect(stdout.startsWith('Timeline'), isFalse); + + _testTimeLineContent( + output: stdout, + drags: _passingDragAmount, + totalExpectedOffset: _passingOffset, + ); + + final htmlLine = stdout + .split('\n') + .firstWhere((line) => line.startsWith('View time line here:')); + + final prefix = isGlobal ? 'global' : 'local'; + expect( + htmlLine.endsWith( + 'timeline-$prefix-live-timeline-without-error-prints-html.html', + ), + isTrue, + ); + } + + static Future liveWithoutErrorPrintsHTMLNoDuplicates({ + bool isGlobal = false, + }) async { + final stdout = await _outputFromDragTestProcess( + isGlobalMode: isGlobal, + title: 'Live timeline - without error, no duplicates, prints HTML', + timelineMode: TimelineMode.live, + drags: _passingDragAmount, + captureStart: + isGlobal ? shared.timelineHeader : '馃敶 - Recording live timeline', + ); + + final lines = stdout.split('\n'); + // In the local test, the default timeline mode is 'record'. + // Switching to live within the test results in a change to the mode, + // which is documented in the output. + expect( + lines.first, + isGlobal ? shared.timelineHeader : '馃敶 - Recording live timeline', + ); + + _testTimeLineContent( + output: stdout, + drags: _passingDragAmount, + totalExpectedOffset: _passingOffset, + ); + + final htmlLine = stdout + .split('\n') + .firstWhere((line) => line.startsWith('View time line here:')); + + final prefix = isGlobal ? 'global' : 'local'; + expect( + htmlLine.endsWith( + 'timeline-$prefix-live-timeline-without-error-no-duplicates-prints-html.html', + ), + isTrue, + ); + } + + static Future liveWithErrorPrintsHTMLNoDuplicates({ + bool isGlobal = false, + }) async { + final stdout = await _outputFromDragTestProcess( + isGlobalMode: isGlobal, + title: 'Live timeline - with error, no duplicates, prints HTML', + timelineMode: TimelineMode.live, + drags: _failingDragAmount, + captureStart: + isGlobal ? shared.timelineHeader : '馃敶 - Recording live timeline', + ); + + final lines = stdout.split('\n'); + // In the local test, the default timeline mode is 'record'. + // Switching to live within the test results in a change to the mode, + // which is documented in the output. + expect( + lines.first, + isGlobal ? shared.timelineHeader : '馃敶 - Recording live timeline', + ); + + _testTimeLineContent( + output: stdout, + drags: _failingDragAmount, + totalExpectedOffset: _failingOffset, + ); + + final htmlLine = stdout + .split('\n') + .firstWhere((line) => line.startsWith('View time line here:')); + + final prefix = isGlobal ? 'global' : 'local'; + expect( + htmlLine.endsWith( + 'timeline-$prefix-live-timeline-with-error-no-duplicates-prints-html.html', + ), + isTrue, + ); + } + + static Future recordWithErrorPrintsHTML({ + bool isGlobal = false, + }) async { + final stdout = await _outputFromDragTestProcess( + title: 'OnError timeline - with error, prints timeline', + timelineMode: TimelineMode.record, + drags: _failingDragAmount, + isGlobalMode: isGlobal, + ); + + _testTimeLineContent( + output: stdout, + drags: _failingDragAmount, + totalExpectedOffset: _failingOffset, + ); + + final htmlLine = stdout + .split('\n') + .firstWhere((line) => line.startsWith('View time line here:')); + final prefix = isGlobal ? 'global' : 'local'; + expect( + htmlLine.endsWith( + 'timeline-$prefix-onerror-timeline-with-error-prints-timeline.html', + ), + isTrue, + ); + } + + static Future _testBody(WidgetTester tester) async { + await tester.pumpWidget(const DragUntilVisibleTestWidget()); + _firstItemSelector.existsOnce(); + _secondItemSelector.doesNotExist(); + await act.dragUntilVisible( + dragStart: _firstItemSelector, + dragTarget: _secondItemSelector, + moveStep: const Offset(0, -100), + maxIteration: _passingDragAmount, + ); + _secondItemSelector.existsOnce(); + } + + static void _testTimeLineContent({ + required String output, + required Offset totalExpectedOffset, + required int drags, + }) { + final isGoingDown = totalExpectedOffset.dy < 0; + final findsWidget = drags == _passingDragAmount; + final eventLines = + output.split('\n').where((line) => line.startsWith('Event:')); + final startEvent = _replaceOffsetWithDxDy(eventLines.first); + final endEvent = + 'Event: Target ${_secondItemSelector.toStringBreadcrumb()} ${findsWidget ? 'found' : 'not found'} after $drags drags. Total dragged offset: $totalExpectedOffset'; + + for (int i = 0; i < 2; i++) { + expect( + RegExp(timelineHeader).allMatches(output).length, + 2, + ); + if (i == 0) { + expect( + startEvent, + 'Event: Scrolling ${isGoingDown ? 'downwards' : 'upwards'} from Offset(dx,dy) in order to find ${_secondItemSelector.toStringBreadcrumb()}.', + ); + } else { + expect( + eventLines.last, + endEvent, + ); + } + expect( + RegExp('Caller: at').allMatches(output).length, + 2, + ); + expect( + RegExp('Screenshot: file:').allMatches(output).length, + 2, + ); + expect( + RegExp('Timestamp: ').allMatches(output).length, + 2, + ); + } + } + + static String _replaceOffsetWithDxDy(String originalString) { + // Regular expression to match 'Offset(any values)' + final RegExp offsetPattern = RegExp(r'Offset\([^)]*\)'); + + // Replace all matches with 'Offset(dx,dy)' + return originalString.replaceAll(offsetPattern, 'Offset(dx,dy)'); + } + + static Future _outputFromDragTestProcess({ + required String title, + required TimelineMode timelineMode, + String captureStart = shared.timelineHeader, + bool isGlobalMode = false, + TimelineMode? globalTimelineModeToSwitch, + required int drags, + }) async { + final testAsString = _testAsString( + title: title, + timelineMode: timelineMode, + isGlobalMode: isGlobalMode, + globalTimelineModeToSwitch: globalTimelineModeToSwitch, + drags: drags, + ); + + return process.runTestInProcessAndCaptureOutPut( + shouldFail: + drags == _failingDragAmount || globalTimelineModeToSwitch != null, + testAsString: testAsString, + captureStart: captureStart, + ); + } + + static String _testAsString({ + required String title, + required TimelineMode timelineMode, + required int drags, + bool isGlobalMode = false, + TimelineMode? globalTimelineModeToSwitch, + }) { + final switchPart = globalTimelineModeToSwitch != null + ? ''' + globalTimelineMode = TimelineMode.${globalTimelineModeToSwitch.toString().split('.').last}; + ''' + : ''; + final testTitle = '${isGlobalMode ? 'Global: ' : 'Local: '}$title'; + + final globalInitiator = + isGlobalMode ? shared.globalTimelineInitiator(timelineMode) : ''; + + final localInitiator = shared.localTimelineInitiator(timelineMode); + + final widgetPart = + File('test/timeline/drag/drag_until_visible_test_widget.dart') + .readAsStringSync(); + return ''' +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; +import 'package:spot/src/timeline/timeline.dart';\n +$widgetPart\n +void main() async { +$globalInitiator + testWidgets("$testTitle", (WidgetTester tester) async { + $localInitiator + $switchPart + await tester.pumpWidget(const DragUntilVisibleTestWidget()); + final firstItem = spotText('Item at index: 3', exact: true)..existsOnce(); + final secondItem = spotText('Item at index: 27', exact: true) + ..doesNotExist(); + await act.dragUntilVisible( + dragStart: firstItem, + dragTarget: secondItem, + maxIteration: $drags, + moveStep: const Offset(0, -100), + ); + secondItem.existsOnce(); + }); +} +'''; + } +} diff --git a/test/timeline/drag/global/global_live_timeline_drag_test.dart b/test/timeline/drag/global/global_live_timeline_drag_test.dart index a484f881..fb9e547b 100644 --- a/test/timeline/drag/global/global_live_timeline_drag_test.dart +++ b/test/timeline/drag/global/global_live_timeline_drag_test.dart @@ -1,17 +1,24 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spot/src/timeline/timeline.dart'; -import '../timeline_drag_test_bodies.dart'; +import '../act_drag_timeline_test_bodies.dart'; void main() { globalTimelineMode = TimelineMode.live; testWidgets('Global: live, without error', (tester) async { - await liveTimelineWithoutErrorsDrag(tester, isGlobal: true); + await ActDragTimelineTestBodies.liveWithoutError( + tester, + isGlobal: true, + ); }); - testWidgets('Global: record, without error', (tester) async { - await liveTimelineWithoutErrorsDrag(tester, isGlobal: true); + test('Global: live - without error, prints HTML, no duplicates', () async { + await ActDragTimelineTestBodies.liveWithoutErrorPrintsHTMLNoDuplicates( + isGlobal: true, + ); }); test('Global: live - with error, prints HTML, no duplicates', () async { - await liveWitErrorPrintsHTMLNoDuplicates(isGlobal: true); + await ActDragTimelineTestBodies.liveWithErrorPrintsHTMLNoDuplicates( + isGlobal: true, + ); }); } diff --git a/test/timeline/drag/global/global_off_timeline_drag_test.dart b/test/timeline/drag/global/global_off_timeline_drag_test.dart index 6b952dd6..4105c3e8 100644 --- a/test/timeline/drag/global/global_off_timeline_drag_test.dart +++ b/test/timeline/drag/global/global_off_timeline_drag_test.dart @@ -1,11 +1,11 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spot/src/timeline/timeline.dart'; -import '../timeline_drag_test_bodies.dart'; +import '../act_drag_timeline_test_bodies.dart'; void main() { globalTimelineMode = TimelineMode.off; testWidgets('Global: off does not record', (tester) async { - await offTimelineWithoutErrorsDrag(tester, isGlobal: true); + await ActDragTimelineTestBodies.offWithoutError(tester, isGlobal: true); }); } diff --git a/test/timeline/drag/global/global_record_timeline_drag_test.dart b/test/timeline/drag/global/global_record_timeline_drag_test.dart index e4a1ddf5..42bf377b 100644 --- a/test/timeline/drag/global/global_record_timeline_drag_test.dart +++ b/test/timeline/drag/global/global_record_timeline_drag_test.dart @@ -1,17 +1,17 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spot/src/timeline/timeline.dart'; -import '../timeline_drag_test_bodies.dart'; +import '../act_drag_timeline_test_bodies.dart'; void main() { globalTimelineMode = TimelineMode.record; testWidgets('Global: record, turn off during test', (tester) async { - await recordTurnOffDuringTestDrag(tester, isGlobal: true); + await ActDragTimelineTestBodies.recordTurnOff(tester, isGlobal: true); }); testWidgets('Global: record - without error', (tester) async { - await recordTurnOffDuringTestDrag(tester, isGlobal: true); + await ActDragTimelineTestBodies.recordNoError(tester, isGlobal: true); }); test('Global: record, with error, prints timeline and html', () async { - await recordWithErrorPrintsHTML(isGlobal: true); + await ActDragTimelineTestBodies.recordWithErrorPrintsHTML(isGlobal: true); }); } diff --git a/test/timeline/drag/local/local_timeline_drag_test.dart b/test/timeline/drag/local/local_timeline_drag_test.dart new file mode 100644 index 00000000..c5b454b9 --- /dev/null +++ b/test/timeline/drag/local/local_timeline_drag_test.dart @@ -0,0 +1,32 @@ +import 'package:flutter_test/flutter_test.dart'; +import '../act_drag_timeline_test_bodies.dart'; + +void main() { + group('Drag Timeline Test', () { + group('Without error', () { + testWidgets('Local: record, without error', (tester) async { + await ActDragTimelineTestBodies.liveWithoutError(tester); + }); + testWidgets('Local: off does not record', (tester) async { + await ActDragTimelineTestBodies.offWithoutError(tester); + }); + testWidgets('Local: record, turn off during test', (tester) async { + await ActDragTimelineTestBodies.recordTurnOff(tester); + }); + testWidgets('Local: record - without error', (tester) async { + await ActDragTimelineTestBodies.recordNoError(tester); + }); + }); + group('Teardown test', () { + test('Local: record, with error, prints timeline and html', () async { + await ActDragTimelineTestBodies.recordWithErrorPrintsHTML(); + }); + test('Local: live - without error, prints HTML', () async { + await ActDragTimelineTestBodies.liveWithoutErrorPrintsHTML(); + }); + test('Local: live - with error, prints HTML, no duplicates', () async { + await ActDragTimelineTestBodies.liveWithErrorPrintsHTMLNoDuplicates(); + }); + }); + }); +} diff --git a/test/timeline/drag/timeline_drag_test.dart b/test/timeline/drag/timeline_drag_test.dart deleted file mode 100644 index 57b5e09d..00000000 --- a/test/timeline/drag/timeline_drag_test.dart +++ /dev/null @@ -1,194 +0,0 @@ -import 'dart:io'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:spot/spot.dart'; -import 'package:test_process/test_process.dart'; -import '../timeline_test_shared.dart' as shared; -import 'timeline_drag_test_bodies.dart'; - -final _secondItemSelector = spotText('Item at index: 27', exact: true); -const _passingDragAmount = 23; -const _passingOffset = Offset(0, -2300); - -const _header = '==================== Timeline Event ===================='; - -String _testAsString({ - required String title, - required TimelineMode timelineMode, - required int drags, -}) { - final widgetPart = - File('test/timeline/drag/drag_until_visible_test_widget.dart') - .readAsStringSync(); - return ''' -import 'package:flutter_test/flutter_test.dart'; -import 'package:spot/spot.dart'; -import 'package:spot/src/timeline/timeline.dart';\n -$widgetPart\n -void main() async { - testWidgets("$title", (WidgetTester tester) async { - ${shared.localTimelineInitiator(timelineMode)}; - await tester.pumpWidget(const DragUntilVisibleTestWidget()); - final firstItem = spotText('Item at index: 3', exact: true)..existsOnce(); - final secondItem = spotText('Item at index: 27', exact: true) - ..doesNotExist(); - await act.dragUntilVisible( - dragStart: firstItem, - dragTarget: secondItem, - maxIteration: $drags, - moveStep: const Offset(0, -100), - ); - secondItem.existsOnce(); - }); -} -'''; -} - -void main() { - group('Drag Timeline Test', () { - group('Without error', () { - testWidgets('Local: record, without error', (tester) async { - await liveTimelineWithoutErrorsDrag(tester); - }); - testWidgets('Local: off does not record', (tester) async { - await offTimelineWithoutErrorsDrag(tester); - }); - testWidgets('Local: record, turn off during test', (tester) async { - await recordTurnOffDuringTestDrag(tester); - }); - testWidgets('Local: record - without error', (tester) async { - await recordNoErrorsDrag(tester); - }); - }); - group('Teardown test', () { - test('Local: record, with error, prints timeline and html', () async { - await recordWithErrorPrintsHTML(); - }); - test('Local: live - without error, prints HTML', () async { - await liveWithoutErrorPrintsHTML(); - }); - test('Local: live - with error, prints HTML, no duplicates', () async { - await liveWitErrorPrintsHTMLNoDuplicates(); - }); - // test('Live timeline - with error, no duplicates, prints HTML', () async { - // final tempDir = Directory.systemTemp.createTempSync(); - // final tempTestFile = File('${tempDir.path}/temp_test.dart'); - // await tempTestFile.writeAsString( - // _testAsString( - // title: 'Live timeline - with error, no duplicates, prints HTML', - // timelineMode: TimelineMode.live, - // shouldFail: true, - // ), - // ); - // final testProcess = - // await TestProcess.start('flutter', ['test', tempTestFile.path]); - // final stdoutBuffer = StringBuffer(); - // bool write = false; - // await for (final line in testProcess.stdoutStream()) { - // if (line.isEmpty) continue; - // if (!write) { - // if (line == _header) { - // write = true; - // } - // if (write) { - // stdoutBuffer.writeln(line); - // } // Error does not happen - // await testProcess.shouldExit(1); - // tempDir.deleteSync(recursive: true); - // final stdout = stdoutBuffer.toString(); - // final timeline = stdout.split('\n'); - // // Does not start with 'Timeline', this only happens on error - // expect(timeline.first, _header); - // expect( - // timeline.second, - // 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', - // ); - // expect( - // timeline[2].startsWith('Caller: at main. file:///'), - // isTrue, - // ); - // expect( - // timeline[3].startsWith( - // 'Screenshot: file:///', - // ), - // isTrue, - // ); - // expect( - // timeline[4].startsWith( - // 'Timestamp:', - // ), - // isTrue, - // ); - // expect( - // timeline[5], - // _separator, - // ); - // final htmlLine = timeline - // .firstWhere((line) => line.startsWith('View time line here:')); - // expect( - // htmlLine.endsWith( - // 'live_timeline_-_with_error,_no_duplicates,_prints_html.html', - // ), - // isTrue, - // ); - // } - // } - // }); - }); - }); -} - -void _testTimeLineContent({ - required String output, - required Offset totalExpectedOffset, - required int drags, - bool runInTestProcess = false, -}) { - final isGoingDown = totalExpectedOffset.dy < 0; - final findsWidget = drags == _passingDragAmount; - final eventLines = - output.split('\n').where((line) => line.startsWith('Event:')); - final startEvent = _replaceOffsetWithDxDy(eventLines.first); - final endEvent = - 'Event: Target ${_secondItemSelector.toStringBreadcrumb()} ${findsWidget ? 'found' : 'not found'} after $drags drags. Total dragged offset: $totalExpectedOffset'; - - for (int i = 0; i < 2; i++) { - expect( - RegExp(_header).allMatches(output).length, - 2, - ); - if (i == 0) { - expect( - startEvent, - 'Event: Scrolling ${isGoingDown ? 'downwards' : 'upwards'} from Offset(dx,dy) in order to find ${_secondItemSelector.toStringBreadcrumb()}.', - ); - } else { - expect( - eventLines.last, - endEvent, - ); - } - expect( - RegExp('Caller: at ${runInTestProcess ? 'main' : '_testBody'}') - .allMatches(output) - .length, - 2, - ); - expect( - RegExp('Screenshot: file:').allMatches(output).length, - 2, - ); - expect( - RegExp('Timestamp: ').allMatches(output).length, - 2, - ); - } -} - -String _replaceOffsetWithDxDy(String originalString) { - // Regular expression to match 'Offset(any values)' - final RegExp offsetPattern = RegExp(r'Offset\([^)]*\)'); - - // Replace all matches with 'Offset(dx,dy)' - return originalString.replaceAll(offsetPattern, 'Offset(dx,dy)'); -} diff --git a/test/timeline/drag/timeline_drag_test_bodies.dart b/test/timeline/drag/timeline_drag_test_bodies.dart deleted file mode 100644 index d80b7579..00000000 --- a/test/timeline/drag/timeline_drag_test_bodies.dart +++ /dev/null @@ -1,321 +0,0 @@ -import 'dart:io'; - -import 'package:dartx/dartx.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:spot/spot.dart'; - -import '../../util/capture_console_output.dart'; -import '../../util/run_test_in_process.dart' as process; -import '../timeline_test_shared.dart' as shared; -import '../timeline_test_shared.dart'; -import 'drag_until_visible_test_widget.dart'; - -final _firstItemSelector = spotText('Item at index: 3', exact: true); -final _secondItemSelector = spotText('Item at index: 27', exact: true); - -const _failingDragAmount = 10; -const _failingOffset = Offset(0, -1000); -const _passingDragAmount = 23; -const _passingOffset = Offset(0, -2300); - -Future liveTimelineWithoutErrorsDrag( - WidgetTester tester, { - bool isGlobal = false, -}) async { - final output = await captureConsoleOutput(() async { - if (!isGlobal) { - timeline.mode = TimelineMode.live; - } - await _testBody(tester); - }); - final hasMessage = output.contains('馃敶 - Recording live timeline'); - - expect(hasMessage, isGlobal ? isFalse : isTrue); - _testTimeLineContent( - output: output, - totalExpectedOffset: _passingOffset, - drags: _passingDragAmount, - ); -} - -Future offTimelineWithoutErrorsDrag( - WidgetTester tester, { - bool isGlobal = false, -}) async { - final output = await captureConsoleOutput(() async { - if (!isGlobal) { - timeline.mode = TimelineMode.off; - } - await _testBody(tester); - }); - final lines = output.split('\n')..removeWhere((line) => line.isEmpty); - const expectedOutput = '鈴革笌 - Timeline recording is off'; - final hasMessage = output.contains(expectedOutput); - expect(hasMessage, isGlobal ? isFalse : isTrue); - expect(lines.length, isGlobal ? 0 : 1); -} - -Future recordTurnOffDuringTestDrag( - WidgetTester tester, { - bool isGlobal = false, -}) async { - final output = await captureConsoleOutput(() async { - timeline.mode = TimelineMode.off; - await _testBody(tester); - }); - final lines = output.split('\n')..removeWhere((line) => line.isEmpty); - expect(lines.length, 1); - expect(lines.first, contains('鈴革笌 - Timeline recording is off')); -} - -Future recordNoErrorsDrag( - WidgetTester tester, { - bool isGlobal = false, -}) async { - final output = await captureConsoleOutput(() async { - // Won't change anything, since it's default. Here to make sure - // nothing is printed when the mode doesn't change. - timeline.mode = TimelineMode.record; - await _testBody(tester); - }); - final lines = output.split('\n')..removeWhere((line) => line.isEmpty); - // Neither timeline output nor HTML link when onError timeline is - // recorded when no error occurs. - expect(lines.length, 0); -} - -Future liveWithoutErrorPrintsHTML({ - bool isGlobal = false, -}) async { - final stdout = await _outputFromDragTestProcess( - title: 'Live timeline - without error, prints HTML', - timelineMode: TimelineMode.live, - drags: _passingDragAmount, - ); - - // Does not start with 'Timeline', this only happens on error - expect(stdout.startsWith('Timeline'), isFalse); - - _testTimeLineContent( - output: stdout, - drags: _passingDragAmount, - totalExpectedOffset: _passingOffset, - ); - - final htmlLine = stdout - .split('\n') - .firstWhere((line) => line.startsWith('View time line here:')); - - final prefix = isGlobal ? 'global' : 'local'; - expect( - htmlLine.endsWith( - 'timeline-$prefix-live-timeline-without-error-prints-html.html', - ), - isTrue, - ); -} - -Future liveWitErrorPrintsHTMLNoDuplicates({ - bool isGlobal = false, -}) async { - final stdout = await _outputFromDragTestProcess( - isGlobalMode: isGlobal, - title: 'Live timeline - with error, no duplicates, prints HTML', - timelineMode: TimelineMode.live, - drags: _failingDragAmount, - captureStart: - isGlobal ? shared.timelineHeader : '馃敶 - Recording live timeline', - ); - - final lines = stdout.split('\n'); - // In the local test, the default timeline mode is 'record'. - // Switching to live within the test results in a change to the mode, - // which is documented in the output. - expect( - lines.first, - isGlobal ? shared.timelineHeader : '馃敶 - Recording live timeline', - ); - - _testTimeLineContent( - output: stdout, - drags: _failingDragAmount, - totalExpectedOffset: _failingOffset, - ); - - final htmlLine = stdout - .split('\n') - .firstWhere((line) => line.startsWith('View time line here:')); - - final prefix = isGlobal ? 'global' : 'local'; - expect( - htmlLine.endsWith( - 'timeline-$prefix-live-timeline-with-error-no-duplicates-prints-html.html', - ), - isTrue, - ); -} - -Future recordWithErrorPrintsHTML({ - bool isGlobal = false, -}) async { - final stdout = await _outputFromDragTestProcess( - title: 'OnError timeline - with error, prints timeline', - timelineMode: TimelineMode.record, - drags: _failingDragAmount, - isGlobalMode: isGlobal, - ); - - _testTimeLineContent( - output: stdout, - drags: _failingDragAmount, - totalExpectedOffset: _failingOffset, - ); - - final htmlLine = stdout - .split('\n') - .firstWhere((line) => line.startsWith('View time line here:')); - final prefix = isGlobal ? 'global' : 'local'; - expect( - htmlLine.endsWith( - 'timeline-$prefix-onerror-timeline-with-error-prints-timeline.html', - ), - isTrue, - ); -} - -Future _testBody(WidgetTester tester) async { - await tester.pumpWidget(const DragUntilVisibleTestWidget()); - _firstItemSelector.existsOnce(); - _secondItemSelector.doesNotExist(); - await act.dragUntilVisible( - dragStart: _firstItemSelector, - dragTarget: _secondItemSelector, - moveStep: const Offset(0, -100), - maxIteration: _passingDragAmount, - ); - _secondItemSelector.existsOnce(); -} - -void _testTimeLineContent({ - required String output, - required Offset totalExpectedOffset, - required int drags, -}) { - final isGoingDown = totalExpectedOffset.dy < 0; - final findsWidget = drags == _passingDragAmount; - final eventLines = - output.split('\n').where((line) => line.startsWith('Event:')); - final startEvent = _replaceOffsetWithDxDy(eventLines.first); - final endEvent = - 'Event: Target ${_secondItemSelector.toStringBreadcrumb()} ${findsWidget ? 'found' : 'not found'} after $drags drags. Total dragged offset: $totalExpectedOffset'; - - for (int i = 0; i < 2; i++) { - expect( - RegExp(timelineHeader).allMatches(output).length, - 2, - ); - if (i == 0) { - expect( - startEvent, - 'Event: Scrolling ${isGoingDown ? 'downwards' : 'upwards'} from Offset(dx,dy) in order to find ${_secondItemSelector.toStringBreadcrumb()}.', - ); - } else { - expect( - eventLines.last, - endEvent, - ); - } - expect( - RegExp('Caller: at').allMatches(output).length, - 2, - ); - expect( - RegExp('Screenshot: file:').allMatches(output).length, - 2, - ); - expect( - RegExp('Timestamp: ').allMatches(output).length, - 2, - ); - } -} - -String _replaceOffsetWithDxDy(String originalString) { - // Regular expression to match 'Offset(any values)' - final RegExp offsetPattern = RegExp(r'Offset\([^)]*\)'); - - // Replace all matches with 'Offset(dx,dy)' - return originalString.replaceAll(offsetPattern, 'Offset(dx,dy)'); -} - -Future _outputFromDragTestProcess({ - required String title, - required TimelineMode timelineMode, - String captureStart = shared.timelineHeader, - bool isGlobalMode = false, - TimelineMode? globalTimelineModeToSwitch, - required int drags, -}) async { - final testAsString = _testAsString( - title: title, - timelineMode: timelineMode, - isGlobalMode: isGlobalMode, - globalTimelineModeToSwitch: globalTimelineModeToSwitch, - drags: drags, - ); - print('testAsString: $testAsString'); - return process.runTestInProcessAndCaptureOutPut( - shouldFail: - drags == _failingDragAmount || globalTimelineModeToSwitch != null, - testAsString: testAsString, - captureStart: captureStart, - ); -} - -String _testAsString({ - required String title, - required TimelineMode timelineMode, - required int drags, - bool isGlobalMode = false, - TimelineMode? globalTimelineModeToSwitch, -}) { - final switchPart = globalTimelineModeToSwitch != null - ? ''' - globalTimelineMode = TimelineMode.${globalTimelineModeToSwitch.toString().split('.').last}; - ''' - : ''; - final testTitle = '${isGlobalMode ? 'Global: ' : 'Local: '}$title'; - - final globalInitiator = - isGlobalMode ? shared.globalTimelineInitiator(timelineMode) : ''; - - final localInitiator = shared.localTimelineInitiator(timelineMode); - - final widgetPart = - File('test/timeline/drag/drag_until_visible_test_widget.dart') - .readAsStringSync(); - return ''' -import 'package:flutter_test/flutter_test.dart'; -import 'package:spot/spot.dart'; -import 'package:spot/src/timeline/timeline.dart';\n -$widgetPart\n -void main() async { -$globalInitiator - testWidgets("$testTitle", (WidgetTester tester) async { - $localInitiator - $switchPart - await tester.pumpWidget(const DragUntilVisibleTestWidget()); - final firstItem = spotText('Item at index: 3', exact: true)..existsOnce(); - final secondItem = spotText('Item at index: 27', exact: true) - ..doesNotExist(); - await act.dragUntilVisible( - dragStart: firstItem, - dragTarget: secondItem, - maxIteration: $drags, - moveStep: const Offset(0, -100), - ); - secondItem.existsOnce(); - }); -} -'''; -} diff --git a/test/timeline/tap/act_tap_timeline_test_bodies.dart b/test/timeline/tap/act_tap_timeline_test_bodies.dart new file mode 100644 index 00000000..46bae5f1 --- /dev/null +++ b/test/timeline/tap/act_tap_timeline_test_bodies.dart @@ -0,0 +1,418 @@ +import 'dart:io'; + +import 'package:dartx/dartx.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; + +import '../../util/capture_console_output.dart'; +import '../../util/run_test_in_process.dart' as process; +import '../timeline_test_shared.dart' as shared; +import 'timeline_tap_test_widget.dart'; + +class ActTapTimelineTestBodies { + static final WidgetSelector _addButtonSelector = spotIcon(Icons.add); + static final _subtractButtonSelector = spotIcon(Icons.remove); + static final _clearButtonSelector = spotIcon(Icons.clear); + static const String _counter3 = 'Counter: 3'; + static const String _counter4 = 'Counter: 4'; + + static Future recordWithoutError({ + required WidgetTester tester, + bool isGlobalMode = false, + }) async { + final output = await captureConsoleOutput(() async { + if (!isGlobalMode) { + timeline.mode = TimelineMode.record; + } + await tester.pumpWidget(const TimelineTestWidget()); + _addButtonSelector.existsOnce(); + spotText(_counter3).existsOnce(); + await act.tap(_addButtonSelector); + spotText(_counter4).existsOnce(); + await act.tap(_subtractButtonSelector); + spotText(_counter3).existsOnce(); + }); + expect(output, isNot(contains('馃敶 - Recording error output timeline'))); + expect( + output, + isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), + ); + expect( + output, + isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), + ); + _testTimeLineContent(output: output, eventCount: 0); + } + + static Future throwOnGlobalTimelineChange({ + bool isGlobalMode = false, + required TimelineMode initialGlobalMode, + required TimelineMode globalTimelineModeToSwitch, + }) async { + final stdout = await _outputFromTapTestProcess( + title: 'Throws if global timeline mode is changed mid test', + timelineMode: initialGlobalMode, + shouldFail: true, + isGlobalMode: isGlobalMode, + captureStart: 'The following StateError was thrown running a test:', + globalTimelineModeToSwitch: globalTimelineModeToSwitch, + ); + final expectedErrorMessage = ''' +Cannot change global timeline mode within a test. +Use "timeline.mode" instead. +Example: timeline.mode = $globalTimelineModeToSwitch; +'''; + expect(stdout, contains(expectedErrorMessage)); + } + + static Future recordWithError({ + bool isGlobalMode = false, + }) async { + final stdout = await _outputFromTapTestProcess( + title: 'OnError timeline - with error, prints timeline', + timelineMode: TimelineMode.record, + shouldFail: true, + isGlobalMode: isGlobalMode, + captureStart: 'Timeline', + ); + + final timeline = stdout.split('\n'); + + expect(timeline.first, 'Timeline'); + expect( + timeline[1], + shared.timelineHeader, + ); + expect( + timeline[2], + 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', + ); + expect( + timeline[3].startsWith('Caller: at main. file:///'), + isTrue, + ); + expect( + timeline[4].startsWith( + 'Screenshot: file:///', + ), + isTrue, + ); + expect( + timeline[5].startsWith( + 'Timestamp:', + ), + isTrue, + ); + expect( + timeline[6], + shared.timelineSeparator, + ); + final prefix = isGlobalMode ? 'global' : 'local'; + final htmlLine = + timeline.firstWhere((line) => line.startsWith('View time line here:')); + expect( + htmlLine.endsWith( + 'timeline-$prefix-onerror-timeline-with-error-prints-timeline.html', + ), + isTrue, + ); + } + + static Future liveWithoutErrorPrintsHtml({ + bool isGlobalMode = false, + }) async { + final stdout = await _outputFromTapTestProcess( + title: 'Live timeline without error prints html', + timelineMode: TimelineMode.live, + isGlobalMode: isGlobalMode, + ); + + final timeline = stdout.split('\n'); +// Does not start with 'Timeline', this only happens on error + expect(timeline.first, shared.timelineHeader); + expect( + timeline.second, + 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', + ); + expect( + timeline[2].startsWith('Caller: at'), + isTrue, + ); + expect( + timeline[3].startsWith( + 'Screenshot: file:///', + ), + isTrue, + ); + expect( + timeline[4].startsWith( + 'Timestamp:', + ), + isTrue, + ); + expect( + timeline[5], + shared.timelineSeparator, + ); + final htmlLine = + timeline.firstWhere((line) => line.startsWith('View time line here:')); + final prefix = isGlobalMode ? 'global' : 'local'; + expect( + htmlLine.endsWith( + 'timeline-$prefix-live-timeline-without-error-prints-html.html', + ), + isTrue, + ); + } + + static Future liveWithErrorNoDuplicatesPrintsHtml({ + bool isGlobalMode = false, + }) async { + final stdout = await _outputFromTapTestProcess( + title: 'Live timeline - with error, no duplicates, prints HTML', + timelineMode: TimelineMode.live, + shouldFail: true, + isGlobalMode: isGlobalMode, + ); + + final timeline = stdout.split('\n'); +// Does not start with 'Timeline', this only happens on error + expect(timeline.first, shared.timelineHeader); + expect( + timeline.second, + 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', + ); + expect( + timeline[2].startsWith('Caller: at main. file:///'), + isTrue, + ); + expect( + timeline[3].startsWith( + 'Screenshot: file:///', + ), + isTrue, + ); + expect( + timeline[4].startsWith( + 'Timestamp:', + ), + isTrue, + ); + expect( + timeline[5], + shared.timelineSeparator, + ); + final prefix = isGlobalMode ? 'global' : 'local'; + final htmlLine = + timeline.firstWhere((line) => line.startsWith('View time line here:')); + expect( + htmlLine.endsWith( + 'timeline-$prefix-live-timeline-with-error-no-duplicates-prints-html.html', + ), + isTrue, + ); + } + + static Future offWithoutError({ + required WidgetTester tester, + bool isGlobalMode = false, + }) async { + final output = await captureConsoleOutput(() async { + if (!isGlobalMode) { + timeline.mode = TimelineMode.off; + } + await tester.pumpWidget(const TimelineTestWidget()); + _addButtonSelector.existsOnce(); + spotText(_counter3).existsOnce(); + await act.tap(_addButtonSelector); + spotText(_counter4).existsOnce(); + await act.tap(_subtractButtonSelector); + spotText(_counter3).existsOnce(); + }); + + expect(output, contains('鈴革笌 - Timeline recording is off')); + expect( + output, + isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), + ); + expect( + output, + isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), + ); + _testTimeLineContent(output: output, eventCount: 0); + } + + static Future liveTurnOffDuringTest({ + required WidgetTester tester, + bool isGlobalMode = false, + }) async { + final output = await captureConsoleOutput(() async { + if (!isGlobalMode) { + timeline.mode = TimelineMode.live; + } + await tester.pumpWidget( + const TimelineTestWidget(), + ); + spotText(_counter3).existsOnce(); + _addButtonSelector.existsOnce(); + await act.tap(_addButtonSelector); + spotText(_counter4).existsOnce(); + await act.tap(_subtractButtonSelector); + spotText(_counter3).existsOnce(); +// Notify that the recording stopped + timeline.mode = TimelineMode.off; + await act.tap(_clearButtonSelector); + spotText('Counter: 0').existsOnce(); + }); + final containsMessage = output.contains('馃敶 - Recording live timeline'); + // Changes in local test since it's `record` by default. Globally it does not + // change since the global mode is already `live`. + expect(containsMessage, isGlobalMode ? isFalse : isTrue); + expect(output, contains('鈴革笌 - Timeline recording is off')); + expect( + output, + contains('Tap ${_addButtonSelector.toStringBreadcrumb()}'), + ); + expect( + output, + contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), + ); +// No further events were added to the timeline, including screenshots + expect( + output, + isNot(contains('Tap ${_clearButtonSelector.toStringBreadcrumb()}')), + ); + _testTimeLineContent(output: output, eventCount: 2); + expect(output, contains('鈴革笌 - Timeline recording is off')); + } + + static Future liveWithoutError({ + required WidgetTester tester, + bool isGlobalMode = false, + }) async { + final output = await captureConsoleOutput(() async { + if (!isGlobalMode) { + timeline.mode = TimelineMode.live; + } + await tester.pumpWidget(const TimelineTestWidget()); + _addButtonSelector.existsOnce(); + spotText(_counter3).existsOnce(); + await act.tap(_addButtonSelector); + spotText(_counter4).existsOnce(); + await act.tap(_subtractButtonSelector); + spotText(_counter3).existsOnce(); + }); + final containsMessage = output.contains('馃敶 - Recording live timeline'); + // Changes in local test since it's `record` by default. Globally it does not + // change since the global mode is already `live`. + expect(containsMessage, isGlobalMode ? isFalse : isTrue); + expect( + output, + contains('Event: Tap ${_addButtonSelector.toStringBreadcrumb()}'), + ); + expect( + output, + contains('Event: Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), + ); + _testTimeLineContent(output: output, eventCount: 2); + } + + static void _testTimeLineContent({ + required String output, + required int eventCount, + }) { + expect( + RegExp(shared.timelineHeader).allMatches(output).length, + eventCount, + ); + expect( + RegExp('Event: Tap Icon Widget with icon:').allMatches(output).length, + eventCount, + ); + final callerParts = output.split('\n').where((line) { + return line.startsWith('Caller: at') && line.contains('file://'); + }).toList(); + expect( + callerParts.length, + eventCount, + ); + expect( + RegExp('Screenshot: file:').allMatches(output).length, + eventCount, + ); + expect( + RegExp('Timestamp: ').allMatches(output).length, + eventCount, + ); + } + + static String _tapTestAsString({ + required String title, + required TimelineMode timelineMode, + bool shouldFail = false, + bool isGlobalMode = false, + TimelineMode? globalTimelineModeToSwitch, + }) { + final switchPart = globalTimelineModeToSwitch != null + ? ''' + globalTimelineMode = TimelineMode.${globalTimelineModeToSwitch.toString().split('.').last}; + ''' + : ''; + final testTitle = '${isGlobalMode ? 'Global: ' : 'Local: '}$title'; + + final globalInitiator = + isGlobalMode ? shared.globalTimelineInitiator(timelineMode) : ''; + + final localInitiator = + isGlobalMode ? '' : shared.localTimelineInitiator(timelineMode); + + final widgetPart = File('test/timeline/tap/timeline_tap_test_widget.dart') + .readAsStringSync(); + return ''' +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; +import 'package:spot/src/timeline/timeline.dart';\n +$widgetPart\n +void main() async { + $globalInitiator + final addButtonSelector = spotIcon(Icons.add); + final subtractButtonSelector = spotIcon(Icons.remove); + testWidgets("$testTitle", (WidgetTester tester) async { + $switchPart + $localInitiator + await tester.pumpWidget(const TimelineTestWidget()); + addButtonSelector.existsOnce(); + spotText(_counter3).existsOnce(); + await act.tap(addButtonSelector); + spotText(_counter4).existsOnce(); + await act.tap(subtractButtonSelector); + spotText(_counter3).existsOnce(); + ${shouldFail ? 'spotText("Counter: 99").existsOnce();' : ''} + }); +} +'''; + } + + static Future _outputFromTapTestProcess({ + required String title, + required TimelineMode timelineMode, + String captureStart = shared.timelineHeader, + bool shouldFail = false, + bool isGlobalMode = false, + TimelineMode? globalTimelineModeToSwitch, + }) async { + final testAsString = _tapTestAsString( + title: title, + timelineMode: timelineMode, + shouldFail: shouldFail, + isGlobalMode: isGlobalMode, + globalTimelineModeToSwitch: globalTimelineModeToSwitch, + ); + return process.runTestInProcessAndCaptureOutPut( + shouldFail: shouldFail, + testAsString: testAsString, + captureStart: captureStart, + ); + } +} diff --git a/test/timeline/tap/global/global_live_timeline_tap_test.dart b/test/timeline/tap/global/global_live_timeline_tap_test.dart index 8bcadc27..93c550fb 100644 --- a/test/timeline/tap/global/global_live_timeline_tap_test.dart +++ b/test/timeline/tap/global/global_live_timeline_tap_test.dart @@ -1,27 +1,27 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spot/src/timeline/timeline.dart'; -import '../timeline_tap_test_bodies.dart' as body; +import '../act_tap_timeline_test_bodies.dart'; void main() { globalTimelineMode = TimelineMode.live; testWidgets('Global: record, without error', (tester) async { - await body.liveWithoutError( + await ActTapTimelineTestBodies.liveWithoutError( tester: tester, isGlobalMode: true, ); }); test('Global: live - without error, prints HTML', () async { - await body.liveWithoutErrorPrintsHtml( + await ActTapTimelineTestBodies.liveWithoutErrorPrintsHtml( isGlobalMode: true, ); }); test('Global: live - with error, no duplicates, prints HTML', () async { - await body.liveWithErrorNoDuplicatesPrintsHtml( + await ActTapTimelineTestBodies.liveWithErrorNoDuplicatesPrintsHtml( isGlobalMode: true, ); }); testWidgets('Global: live, turn off during test', (tester) async { - await body.liveTurnOffDuringTest( + await ActTapTimelineTestBodies.liveTurnOffDuringTest( isGlobalMode: true, tester: tester, ); diff --git a/test/timeline/tap/global/global_off_timeline_tap_test.dart b/test/timeline/tap/global/global_off_timeline_tap_test.dart index c03a5e9a..9daa5d12 100644 --- a/test/timeline/tap/global/global_off_timeline_tap_test.dart +++ b/test/timeline/tap/global/global_off_timeline_tap_test.dart @@ -1,11 +1,11 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spot/src/timeline/timeline.dart'; -import '../timeline_tap_test_bodies.dart' as body; +import '../act_tap_timeline_test_bodies.dart'; void main() { globalTimelineMode = TimelineMode.off; testWidgets('Global: off, without error', (tester) async { - await body.offWithoutError( + await ActTapTimelineTestBodies.offWithoutError( tester: tester, isGlobalMode: true, ); diff --git a/test/timeline/tap/global/global_record_timeline_tap_test.dart b/test/timeline/tap/global/global_record_timeline_tap_test.dart index 27d8f3d9..165ac11f 100644 --- a/test/timeline/tap/global/global_record_timeline_tap_test.dart +++ b/test/timeline/tap/global/global_record_timeline_tap_test.dart @@ -1,16 +1,16 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spot/src/timeline/timeline.dart'; -import '../timeline_tap_test_bodies.dart' as body; +import '../act_tap_timeline_test_bodies.dart'; void main() { globalTimelineMode = TimelineMode.record; testWidgets('Global: record, without error', (tester) async { - await body.recordWithoutError( + await ActTapTimelineTestBodies.recordWithoutError( tester: tester, isGlobalMode: true, ); }); test('Global: record, with error', () async { - await body.recordWithError(isGlobalMode: true); + await ActTapTimelineTestBodies.recordWithError(isGlobalMode: true); }); } diff --git a/test/timeline/tap/local/local_timeline_tap_test.dart b/test/timeline/tap/local/local_timeline_tap_test.dart index 0a1526e0..24051484 100644 --- a/test/timeline/tap/local/local_timeline_tap_test.dart +++ b/test/timeline/tap/local/local_timeline_tap_test.dart @@ -1,38 +1,38 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spot/src/timeline/timeline.dart'; -import '../timeline_tap_test_bodies.dart' as body; +import '../act_tap_timeline_test_bodies.dart'; void main() { group('Override global timeline', () { testWidgets('Local: live, without error', (tester) async { - await body.liveWithoutError(tester: tester); + await ActTapTimelineTestBodies.liveWithoutError(tester: tester); }); testWidgets('Local: off, without error', (tester) async { - await body.offWithoutError(tester: tester); + await ActTapTimelineTestBodies.offWithoutError(tester: tester); }); testWidgets('Local: live, turn off during test', (tester) async { - await body.liveTurnOffDuringTest(tester: tester); + await ActTapTimelineTestBodies.liveTurnOffDuringTest(tester: tester); }); }); group('Print on teardown', () { testWidgets('Local: record, without error', (tester) async { - await body.recordWithoutError(tester: tester); + await ActTapTimelineTestBodies.recordWithoutError(tester: tester); }); test('Local: record, with error', () async { - await body.recordWithError(); + await ActTapTimelineTestBodies.recordWithError(); }); test('Local: live - without error, prints HTML', () async { - await body.liveWithoutErrorPrintsHtml(); + await ActTapTimelineTestBodies.liveWithoutErrorPrintsHtml(); }); test('Local: live - with error, no duplicates, prints HTML', () async { - await body.liveWithErrorNoDuplicatesPrintsHtml(); + await ActTapTimelineTestBodies.liveWithErrorNoDuplicatesPrintsHtml(); }); }); test('Throws when global mode is changed during test', () async { - await body.throwOnGlobalTimelineChange( + await ActTapTimelineTestBodies.throwOnGlobalTimelineChange( initialGlobalMode: TimelineMode.live, globalTimelineModeToSwitch: TimelineMode.record, ); diff --git a/test/timeline/tap/timeline_tap_test_bodies.dart b/test/timeline/tap/timeline_tap_test_bodies.dart deleted file mode 100644 index 1959bce8..00000000 --- a/test/timeline/tap/timeline_tap_test_bodies.dart +++ /dev/null @@ -1,417 +0,0 @@ -import 'dart:io'; - -import 'package:dartx/dartx.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:spot/spot.dart'; - -import '../../util/capture_console_output.dart'; -import '../../util/run_test_in_process.dart' as process; -import '../timeline_test_shared.dart' as shared; -import 'timeline_tap_test_widget.dart'; - -final WidgetSelector _addButtonSelector = spotIcon(Icons.add); -final _subtractButtonSelector = spotIcon(Icons.remove); -final _clearButtonSelector = spotIcon(Icons.clear); -const String counter3 = 'Counter: 3'; -const String counter4 = 'Counter: 4'; -const String counter99 = 'Counter: 99'; - -Future recordWithoutError({ - required WidgetTester tester, - bool isGlobalMode = false, -}) async { - final output = await captureConsoleOutput(() async { - if (!isGlobalMode) { - timeline.mode = TimelineMode.record; - } - await tester.pumpWidget(const TimelineTestWidget()); - _addButtonSelector.existsOnce(); - spotText('Counter: 3').existsOnce(); - await act.tap(_addButtonSelector); - spotText('Counter: 4').existsOnce(); - await act.tap(_subtractButtonSelector); - spotText('Counter: 3').existsOnce(); - }); - expect(output, isNot(contains('馃敶 - Recording error output timeline'))); - expect( - output, - isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), - ); - expect( - output, - isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), - ); - _testTimeLineContent(output: output, eventCount: 0); -} - -Future throwOnGlobalTimelineChange({ - bool isGlobalMode = false, - required TimelineMode initialGlobalMode, - required TimelineMode globalTimelineModeToSwitch, -}) async { - final stdout = await _outputFromTapTestProcess( - title: 'Throws if global timeline mode is changed mid test', - timelineMode: initialGlobalMode, - shouldFail: true, - isGlobalMode: isGlobalMode, - captureStart: 'The following StateError was thrown running a test:', - globalTimelineModeToSwitch: globalTimelineModeToSwitch, - ); - final expectedErrorMessage = ''' -Cannot change global timeline mode within a test. -Use "timeline.mode" instead. -Example: timeline.mode = $globalTimelineModeToSwitch; -'''; - expect(stdout, contains(expectedErrorMessage)); -} - -Future recordWithError({ - bool isGlobalMode = false, -}) async { - final stdout = await _outputFromTapTestProcess( - title: 'OnError timeline - with error, prints timeline', - timelineMode: TimelineMode.record, - shouldFail: true, - isGlobalMode: isGlobalMode, - captureStart: 'Timeline', - ); - - final timeline = stdout.split('\n'); - - expect(timeline.first, 'Timeline'); - expect( - timeline[1], - shared.timelineHeader, - ); - expect( - timeline[2], - 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', - ); - expect( - timeline[3].startsWith('Caller: at main. file:///'), - isTrue, - ); - expect( - timeline[4].startsWith( - 'Screenshot: file:///', - ), - isTrue, - ); - expect( - timeline[5].startsWith( - 'Timestamp:', - ), - isTrue, - ); - expect( - timeline[6], - shared.timelineSeparator, - ); - final prefix = isGlobalMode ? 'global' : 'local'; - final htmlLine = - timeline.firstWhere((line) => line.startsWith('View time line here:')); - expect( - htmlLine.endsWith( - 'timeline-$prefix-onerror-timeline-with-error-prints-timeline.html', - ), - isTrue, - ); -} - -Future liveWithoutErrorPrintsHtml({ - bool isGlobalMode = false, -}) async { - final stdout = await _outputFromTapTestProcess( - title: 'Live timeline without error prints html', - timelineMode: TimelineMode.live, - isGlobalMode: isGlobalMode, - ); - - final timeline = stdout.split('\n'); -// Does not start with 'Timeline', this only happens on error - expect(timeline.first, shared.timelineHeader); - expect( - timeline.second, - 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', - ); - expect( - timeline[2].startsWith('Caller: at'), - isTrue, - ); - expect( - timeline[3].startsWith( - 'Screenshot: file:///', - ), - isTrue, - ); - expect( - timeline[4].startsWith( - 'Timestamp:', - ), - isTrue, - ); - expect( - timeline[5], - shared.timelineSeparator, - ); - final htmlLine = - timeline.firstWhere((line) => line.startsWith('View time line here:')); - final prefix = isGlobalMode ? 'global' : 'local'; - expect( - htmlLine.endsWith( - 'timeline-$prefix-live-timeline-without-error-prints-html.html', - ), - isTrue, - ); -} - -Future liveWithErrorNoDuplicatesPrintsHtml({ - bool isGlobalMode = false, -}) async { - final stdout = await _outputFromTapTestProcess( - title: 'Live timeline - with error, no duplicates, prints HTML', - timelineMode: TimelineMode.live, - shouldFail: true, - isGlobalMode: isGlobalMode, - ); - - final timeline = stdout.split('\n'); -// Does not start with 'Timeline', this only happens on error - expect(timeline.first, shared.timelineHeader); - expect( - timeline.second, - 'Event: Tap Icon Widget with icon: "IconData(U+0E047)"', - ); - expect( - timeline[2].startsWith('Caller: at main. file:///'), - isTrue, - ); - expect( - timeline[3].startsWith( - 'Screenshot: file:///', - ), - isTrue, - ); - expect( - timeline[4].startsWith( - 'Timestamp:', - ), - isTrue, - ); - expect( - timeline[5], - shared.timelineSeparator, - ); - final prefix = isGlobalMode ? 'global' : 'local'; - final htmlLine = - timeline.firstWhere((line) => line.startsWith('View time line here:')); - expect( - htmlLine.endsWith( - 'timeline-$prefix-live-timeline-with-error-no-duplicates-prints-html.html', - ), - isTrue, - ); -} - -Future offWithoutError({ - required WidgetTester tester, - bool isGlobalMode = false, -}) async { - final output = await captureConsoleOutput(() async { - if (!isGlobalMode) { - timeline.mode = TimelineMode.off; - } - await tester.pumpWidget(const TimelineTestWidget()); - _addButtonSelector.existsOnce(); - spotText('Counter: 3').existsOnce(); - await act.tap(_addButtonSelector); - spotText('Counter: 4').existsOnce(); - await act.tap(_subtractButtonSelector); - spotText('Counter: 3').existsOnce(); - }); - - expect(output, contains('鈴革笌 - Timeline recording is off')); - expect( - output, - isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), - ); - expect( - output, - isNot(contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}')), - ); - _testTimeLineContent(output: output, eventCount: 0); -} - -Future liveTurnOffDuringTest({ - required WidgetTester tester, - bool isGlobalMode = false, -}) async { - final output = await captureConsoleOutput(() async { - if (!isGlobalMode) { - timeline.mode = TimelineMode.live; - } - await tester.pumpWidget( - const TimelineTestWidget(), - ); - spotText('Counter: 3').existsOnce(); - _addButtonSelector.existsOnce(); - await act.tap(_addButtonSelector); - spotText('Counter: 4').existsOnce(); - await act.tap(_subtractButtonSelector); - spotText('Counter: 3').existsOnce(); -// Notify that the recording stopped - timeline.mode = TimelineMode.off; - await act.tap(_clearButtonSelector); - spotText('Counter: 0').existsOnce(); - }); - final containsMessage = output.contains('馃敶 - Recording live timeline'); - // Changes in local test since it's `record` by default. Globally it does not - // change since the global mode is already `live`. - expect(containsMessage, isGlobalMode ? isFalse : isTrue); - expect(output, contains('鈴革笌 - Timeline recording is off')); - expect( - output, - contains('Tap ${_addButtonSelector.toStringBreadcrumb()}'), - ); - expect( - output, - contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), - ); -// No further events were added to the timeline, including screenshots - expect( - output, - isNot(contains('Tap ${_clearButtonSelector.toStringBreadcrumb()}')), - ); - _testTimeLineContent(output: output, eventCount: 2); - expect(output, contains('鈴革笌 - Timeline recording is off')); -} - -Future liveWithoutError({ - required WidgetTester tester, - bool isGlobalMode = false, -}) async { - final output = await captureConsoleOutput(() async { - if (!isGlobalMode) { - timeline.mode = TimelineMode.live; - } - await tester.pumpWidget(const TimelineTestWidget()); - _addButtonSelector.existsOnce(); - spotText('Counter: 3').existsOnce(); - await act.tap(_addButtonSelector); - spotText('Counter: 4').existsOnce(); - await act.tap(_subtractButtonSelector); - spotText('Counter: 3').existsOnce(); - }); - final containsMessage = output.contains('馃敶 - Recording live timeline'); - // Changes in local test since it's `record` by default. Globally it does not - // change since the global mode is already `live`. - expect(containsMessage, isGlobalMode ? isFalse : isTrue); - expect( - output, - contains('Event: Tap ${_addButtonSelector.toStringBreadcrumb()}'), - ); - expect( - output, - contains('Event: Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), - ); - _testTimeLineContent(output: output, eventCount: 2); -} - -void _testTimeLineContent({ - required String output, - required int eventCount, -}) { - expect( - RegExp(shared.timelineHeader).allMatches(output).length, - eventCount, - ); - expect( - RegExp('Event: Tap Icon Widget with icon:').allMatches(output).length, - eventCount, - ); - final callerParts = output.split('\n').where((line) { - return line.startsWith('Caller: at') && line.contains('file://'); - }).toList(); - expect( - callerParts.length, - eventCount, - ); - expect( - RegExp('Screenshot: file:').allMatches(output).length, - eventCount, - ); - expect( - RegExp('Timestamp: ').allMatches(output).length, - eventCount, - ); -} - -String _tapTestAsString({ - required String title, - required TimelineMode timelineMode, - bool shouldFail = false, - bool isGlobalMode = false, - TimelineMode? globalTimelineModeToSwitch, -}) { - final switchPart = globalTimelineModeToSwitch != null - ? ''' - globalTimelineMode = TimelineMode.${globalTimelineModeToSwitch.toString().split('.').last}; - ''' - : ''; - final testTitle = '${isGlobalMode ? 'Global: ' : 'Local: '}$title'; - - final globalInitiator = - isGlobalMode ? shared.globalTimelineInitiator(timelineMode) : ''; - - final localInitiator = - isGlobalMode ? '' : shared.localTimelineInitiator(timelineMode); - - final widgetPart = File('test/timeline/tap/timeline_tap_test_widget.dart') - .readAsStringSync(); - return ''' -import 'package:flutter_test/flutter_test.dart'; -import 'package:spot/spot.dart'; -import 'package:spot/src/timeline/timeline.dart';\n -$widgetPart\n -void main() async { - $globalInitiator - final addButtonSelector = spotIcon(Icons.add); - final subtractButtonSelector = spotIcon(Icons.remove); - testWidgets("$testTitle", (WidgetTester tester) async { - $switchPart - $localInitiator - await tester.pumpWidget(const TimelineTestWidget()); - addButtonSelector.existsOnce(); - spotText('Counter: 3').existsOnce(); - await act.tap(addButtonSelector); - spotText('Counter: 4').existsOnce(); - await act.tap(subtractButtonSelector); - spotText('Counter: 3').existsOnce(); - ${shouldFail ? 'spotText("Counter: 99").existsOnce();' : ''} - }); -} -'''; -} - -Future _outputFromTapTestProcess({ - required String title, - required TimelineMode timelineMode, - String captureStart = shared.timelineHeader, - bool shouldFail = false, - bool isGlobalMode = false, - TimelineMode? globalTimelineModeToSwitch, -}) async { - final testAsString = _tapTestAsString( - title: title, - timelineMode: timelineMode, - shouldFail: shouldFail, - isGlobalMode: isGlobalMode, - globalTimelineModeToSwitch: globalTimelineModeToSwitch, - ); - return process.runTestInProcessAndCaptureOutPut( - shouldFail: shouldFail, - testAsString: testAsString, - captureStart: captureStart, - ); -} From 8f5d6fe3680664e633e182198e7654648a128ec9 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 3 Jul 2024 03:19:10 +0200 Subject: [PATCH 111/119] Prepare live test --- lib/src/act/act.dart | 46 +++++++++++-------- .../tap/act_tap_timeline_test_bodies.dart | 32 ++++++------- .../global/global_live_timeline_tap_test.dart | 2 +- 3 files changed, 44 insertions(+), 36 deletions(-) diff --git a/lib/src/act/act.dart b/lib/src/act/act.dart index 730982a7..d6f6fb0c 100644 --- a/lib/src/act/act.dart +++ b/lib/src/act/act.dart @@ -109,20 +109,25 @@ class Act { _reportPartialCoverage(pokablePositions, snapshot); final positionToTap = pokablePositions.mostCenterHittablePosition!; + final binding = TestWidgetsFlutterBinding.instance; if (timeline.mode != TimelineMode.off) { - final screenshot = await takeScreenshotWithCrosshair( - centerPosition: positionToTap, - ); - timeline.addScreenshot( - screenshot, - name: 'Tap ${selector.toStringBreadcrumb()}', - eventType: const TimelineEventType(label: 'tap'), - ); + final eventName = 'Tap ${selector.toStringBreadcrumb()}'; + const String label = 'tap'; + if (binding is! LiveTestWidgetsFlutterBinding) { + final screenshot = await takeScreenshotWithCrosshair( + centerPosition: positionToTap, + ); + timeline.addScreenshot( + screenshot, + name: eventName, + eventType: const TimelineEventType(label: label), + ); + } else { + timeline.addEvent(name: eventName, eventType: label); + } } - final binding = TestWidgetsFlutterBinding.instance; - // Finally, tap the widget by sending a down and up event. final downEvent = PointerDownEvent(position: positionToTap); binding.handlePointerEvent(downEvent); @@ -212,14 +217,19 @@ class Act { required String name, }) async { if (timeline.mode != TimelineMode.off) { - final screenshot = await takeScreenshotWithCrosshair( - centerPosition: dragPosition, - ); - timeline.addScreenshot( - screenshot, - name: name, - eventType: const TimelineEventType(label: 'drag'), - ); + const String label = 'drag'; + if (binding is! LiveTestWidgetsFlutterBinding) { + final screenshot = await takeScreenshotWithCrosshair( + centerPosition: dragPosition, + ); + timeline.addScreenshot( + screenshot, + name: name, + eventType: const TimelineEventType(label: label), + ); + } else { + timeline.addEvent(name: name, eventType: label); + } } } diff --git a/test/timeline/tap/act_tap_timeline_test_bodies.dart b/test/timeline/tap/act_tap_timeline_test_bodies.dart index 46bae5f1..d2d9f598 100644 --- a/test/timeline/tap/act_tap_timeline_test_bodies.dart +++ b/test/timeline/tap/act_tap_timeline_test_bodies.dart @@ -14,8 +14,6 @@ class ActTapTimelineTestBodies { static final WidgetSelector _addButtonSelector = spotIcon(Icons.add); static final _subtractButtonSelector = spotIcon(Icons.remove); static final _clearButtonSelector = spotIcon(Icons.clear); - static const String _counter3 = 'Counter: 3'; - static const String _counter4 = 'Counter: 4'; static Future recordWithoutError({ required WidgetTester tester, @@ -27,11 +25,11 @@ class ActTapTimelineTestBodies { } await tester.pumpWidget(const TimelineTestWidget()); _addButtonSelector.existsOnce(); - spotText(_counter3).existsOnce(); + spotText('Counter: 3').existsOnce(); await act.tap(_addButtonSelector); - spotText(_counter4).existsOnce(); + spotText('Counter: 4').existsOnce(); await act.tap(_subtractButtonSelector); - spotText(_counter3).existsOnce(); + spotText('Counter: 3').existsOnce(); }); expect(output, isNot(contains('馃敶 - Recording error output timeline'))); expect( @@ -224,11 +222,11 @@ Example: timeline.mode = $globalTimelineModeToSwitch; } await tester.pumpWidget(const TimelineTestWidget()); _addButtonSelector.existsOnce(); - spotText(_counter3).existsOnce(); + spotText('Counter: 3').existsOnce(); await act.tap(_addButtonSelector); - spotText(_counter4).existsOnce(); + spotText('Counter: 4').existsOnce(); await act.tap(_subtractButtonSelector); - spotText(_counter3).existsOnce(); + spotText('Counter: 3').existsOnce(); }); expect(output, contains('鈴革笌 - Timeline recording is off')); @@ -254,12 +252,12 @@ Example: timeline.mode = $globalTimelineModeToSwitch; await tester.pumpWidget( const TimelineTestWidget(), ); - spotText(_counter3).existsOnce(); + spotText('Counter: 3').existsOnce(); _addButtonSelector.existsOnce(); await act.tap(_addButtonSelector); - spotText(_counter4).existsOnce(); + spotText('Counter: 4').existsOnce(); await act.tap(_subtractButtonSelector); - spotText(_counter3).existsOnce(); + spotText('Counter: 3').existsOnce(); // Notify that the recording stopped timeline.mode = TimelineMode.off; await act.tap(_clearButtonSelector); @@ -297,11 +295,11 @@ Example: timeline.mode = $globalTimelineModeToSwitch; } await tester.pumpWidget(const TimelineTestWidget()); _addButtonSelector.existsOnce(); - spotText(_counter3).existsOnce(); + spotText('Counter: 3').existsOnce(); await act.tap(_addButtonSelector); - spotText(_counter4).existsOnce(); + spotText('Counter: 4').existsOnce(); await act.tap(_subtractButtonSelector); - spotText(_counter3).existsOnce(); + spotText('Counter: 3').existsOnce(); }); final containsMessage = output.contains('馃敶 - Recording live timeline'); // Changes in local test since it's `record` by default. Globally it does not @@ -383,11 +381,11 @@ void main() async { $localInitiator await tester.pumpWidget(const TimelineTestWidget()); addButtonSelector.existsOnce(); - spotText(_counter3).existsOnce(); + spotText('Counter: 3').existsOnce(); await act.tap(addButtonSelector); - spotText(_counter4).existsOnce(); + spotText('Counter: 4').existsOnce(); await act.tap(subtractButtonSelector); - spotText(_counter3).existsOnce(); + spotText('Counter: 3').existsOnce(); ${shouldFail ? 'spotText("Counter: 99").existsOnce();' : ''} }); } diff --git a/test/timeline/tap/global/global_live_timeline_tap_test.dart b/test/timeline/tap/global/global_live_timeline_tap_test.dart index 93c550fb..0ee3c679 100644 --- a/test/timeline/tap/global/global_live_timeline_tap_test.dart +++ b/test/timeline/tap/global/global_live_timeline_tap_test.dart @@ -4,7 +4,7 @@ import '../act_tap_timeline_test_bodies.dart'; void main() { globalTimelineMode = TimelineMode.live; - testWidgets('Global: record, without error', (tester) async { + testWidgets('Global: live, without error', (tester) async { await ActTapTimelineTestBodies.liveWithoutError( tester: tester, isGlobalMode: true, From 8fc5ccffc08d7f8c42d7d69526dcaa9c7dcaef0d Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 3 Jul 2024 03:19:17 +0200 Subject: [PATCH 112/119] Dispose plain image --- lib/src/screenshot/screenshot.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/screenshot/screenshot.dart b/lib/src/screenshot/screenshot.dart index 3e65ed63..3cd898db 100644 --- a/lib/src/screenshot/screenshot.dart +++ b/lib/src/screenshot/screenshot.dart @@ -161,6 +161,7 @@ Future _createScreenshot({ throw 'Could not take screenshot'; } image = byteData.buffer.asUint8List(); + plainImage.dispose(); }); final spotTempDir = Directory.systemTemp.directory('spot'); From c9dcca1062ab6852688c52fa85a746e37afc1a50 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 24 Jul 2024 15:41:12 +0200 Subject: [PATCH 113/119] Await _alwaysPropagateDevicePointerEvents --- lib/src/act/act.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/src/act/act.dart b/lib/src/act/act.dart index d6f6fb0c..face2277 100644 --- a/lib/src/act/act.dart +++ b/lib/src/act/act.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:dartx/dartx_io.dart'; @@ -85,7 +86,7 @@ class Act { final snapshot = selector.snapshot()..existsOnce(); return TestAsyncUtils.guard(() async { - return _alwaysPropagateDevicePointerEvents(() async { + return await _alwaysPropagateDevicePointerEvents(() async { final renderBox = _getRenderBoxOrThrow(selector); // Before tapping the widget, we need to make sure that the widget is @@ -170,7 +171,7 @@ class Act { final snapshot = dragStart.snapshot()..existsOnce(); return TestAsyncUtils.guard(() async { - return _alwaysPropagateDevicePointerEvents(() async { + return await _alwaysPropagateDevicePointerEvents(() async { final renderBox = _getRenderBoxOrThrow(dragStart); final binding = TestWidgetsFlutterBinding.instance; @@ -713,7 +714,9 @@ extension on HitTestEntry { /// widgets and are not intercepted by [LiveTestWidgetsFlutterBinding]. /// /// See [LiveTestWidgetsFlutterBinding.shouldPropagateDevicePointerEvents]. -T _alwaysPropagateDevicePointerEvents(T Function() block) { +Future _alwaysPropagateDevicePointerEvents( + FutureOr Function() block, +) async { final binding = WidgetsBinding.instance; final live = binding is LiveTestWidgetsFlutterBinding; @@ -727,7 +730,7 @@ T _alwaysPropagateDevicePointerEvents(T Function() block) { binding.shouldPropagateDevicePointerEvents = true; } try { - return block(); + return await block(); } finally { if (live) { binding.shouldPropagateDevicePointerEvents = previousPropagateValue; From 80d788a7b1691c502e55cb99bf01ea1fc165a77f Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 24 Jul 2024 15:41:37 +0200 Subject: [PATCH 114/119] Add act drag live test --- test/act/act_drag_live_test.dart | 11 ++ test/act/act_drag_test.dart | 138 +++++++++--------- .../drag/act_drag_timeline_test_bodies.dart | 11 +- .../tap/act_tap_timeline_test_bodies.dart | 29 ++-- test/util/run_test_in_process.dart | 6 +- 5 files changed, 112 insertions(+), 83 deletions(-) create mode 100644 test/act/act_drag_live_test.dart diff --git a/test/act/act_drag_live_test.dart b/test/act/act_drag_live_test.dart new file mode 100644 index 00000000..7a5f99e0 --- /dev/null +++ b/test/act/act_drag_live_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'act_drag_test.dart'; + +void main() { + // Runs the tests as executed with `flutter run` (on a device) + LiveTestWidgetsFlutterBinding.ensureInitialized(); + assert(WidgetsBinding.instance is LiveTestWidgetsFlutterBinding); + group('Drag with LiveTestWidgetsFlutterBinding', dragTests); +} diff --git a/test/act/act_drag_test.dart b/test/act/act_drag_test.dart index be29b862..7b8ac734 100644 --- a/test/act/act_drag_test.dart +++ b/test/act/act_drag_test.dart @@ -1,82 +1,88 @@ +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import '../timeline/drag/drag_until_visible_test_widget.dart'; void main() { - group('Drag Events', () { - testWidgets('Finds widget after dragging', (tester) async { - timeline.mode = TimelineMode.live; - await tester.pumpWidget( - const DragUntilVisibleTestWidget(), - ); + // Runs the tests as executed with `flutter test` + AutomatedTestWidgetsFlutterBinding.ensureInitialized(); + assert(WidgetsBinding.instance is! LiveTestWidgetsFlutterBinding); + group('Drag with AutomatedTestWidgetsFlutterBinding', dragTests); +} - final firstItem = spotText('Item at index: 3', exact: true)..existsOnce(); - final secondItem = spotText('Item at index: 27', exact: true) - ..doesNotExist(); - await act.dragUntilVisible( - dragStart: firstItem, - dragTarget: secondItem, - maxIteration: 30, - moveStep: const Offset(0, -100), - ); - secondItem.existsOnce(); - }); +void dragTests() { + testWidgets('Finds widget after dragging', (tester) async { + timeline.mode = TimelineMode.live; + await tester.pumpWidget( + const DragUntilVisibleTestWidget(), + ); - testWidgets('Finds widgets after dragging down and up', (tester) async { - await tester.pumpWidget( - const DragUntilVisibleTestWidget(), - ); + final firstItem = spotText('Item at index: 3', exact: true)..existsOnce(); + final secondItem = spotText('Item at index: 27', exact: true) + ..doesNotExist(); + await act.dragUntilVisible( + dragStart: firstItem, + dragTarget: secondItem, + maxIteration: 30, + moveStep: const Offset(0, -100), + ); + secondItem.existsOnce(); + }); - final firstItem = spotText('Item at index: 3', exact: true)..existsOnce(); - final secondItem = spotText('Item at index: 27', exact: true) - ..doesNotExist(); - await act.dragUntilVisible( - dragStart: firstItem, - dragTarget: secondItem, - maxIteration: 30, - moveStep: const Offset(0, -100), - ); - secondItem.existsOnce(); - firstItem.doesNotExist(); - await tester.pumpAndSettle(); - await act.dragUntilVisible( - dragStart: secondItem, - dragTarget: firstItem, - moveStep: const Offset(0, 100), - ); - await tester.pumpAndSettle(); - firstItem.existsOnce(); - secondItem.doesNotExist(); - }); + testWidgets('Finds widgets after dragging down and up', (tester) async { + await tester.pumpWidget( + const DragUntilVisibleTestWidget(), + ); + + final firstItem = spotText('Item at index: 3', exact: true)..existsOnce(); + final secondItem = spotText('Item at index: 27', exact: true) + ..doesNotExist(); + await act.dragUntilVisible( + dragStart: firstItem, + dragTarget: secondItem, + maxIteration: 30, + moveStep: const Offset(0, -100), + ); + secondItem.existsOnce(); + firstItem.doesNotExist(); + await tester.pumpAndSettle(); + await act.dragUntilVisible( + dragStart: secondItem, + dragTarget: firstItem, + moveStep: const Offset(0, 100), + ); + await tester.pumpAndSettle(); + firstItem.existsOnce(); + secondItem.doesNotExist(); + }); - testWidgets('Throws TestFailure if not found', (tester) async { - await tester.pumpWidget( - const DragUntilVisibleTestWidget(), - ); + testWidgets('Throws TestFailure if not found', (tester) async { + await tester.pumpWidget( + const DragUntilVisibleTestWidget(), + ); - final firstItem = spotText('Item at index: 3', exact: true)..existsOnce(); - final secondItem = spotText('Item at index: 27', exact: true) - ..doesNotExist(); + final firstItem = spotText('Item at index: 3', exact: true)..existsOnce(); + final secondItem = spotText('Item at index: 27', exact: true) + ..doesNotExist(); - const expectedErrorMessage = - 'Widget with text with text "Item at index: 27" is not visible after dragging 10 times and a total dragged offset of Offset(0.0, -1000.0).'; + const expectedErrorMessage = + 'Widget with text with text "Item at index: 27" is not visible after dragging 10 times and a total dragged offset of Offset(0.0, -1000.0).'; - await expectLater( - () => act.dragUntilVisible( - dragStart: firstItem, - dragTarget: secondItem, - maxIteration: 10, - moveStep: const Offset(0, -100), - ), - throwsA( - isA().having( - (error) => error.message, - 'message', - expectedErrorMessage, - ), + await expectLater( + () => act.dragUntilVisible( + dragStart: firstItem, + dragTarget: secondItem, + maxIteration: 10, + moveStep: const Offset(0, -100), + ), + throwsA( + isA().having( + (error) => error.message, + 'message', + expectedErrorMessage, ), - ); - }); + ), + ); }); } diff --git a/test/timeline/drag/act_drag_timeline_test_bodies.dart b/test/timeline/drag/act_drag_timeline_test_bodies.dart index 739294e2..e1e9b04a 100644 --- a/test/timeline/drag/act_drag_timeline_test_bodies.dart +++ b/test/timeline/drag/act_drag_timeline_test_bodies.dart @@ -90,6 +90,7 @@ class ActDragTimelineTestBodies { title: 'Live timeline - without error, prints HTML', timelineMode: TimelineMode.live, drags: _passingDragAmount, + captureStart: ['Timeline', shared.timelineHeader], ); // Does not start with 'Timeline', this only happens on error @@ -122,8 +123,7 @@ class ActDragTimelineTestBodies { title: 'Live timeline - without error, no duplicates, prints HTML', timelineMode: TimelineMode.live, drags: _passingDragAmount, - captureStart: - isGlobal ? shared.timelineHeader : '馃敶 - Recording live timeline', + captureStart: [shared.timelineHeader, '馃敶 - Recording live timeline'], ); final lines = stdout.split('\n'); @@ -162,8 +162,9 @@ class ActDragTimelineTestBodies { title: 'Live timeline - with error, no duplicates, prints HTML', timelineMode: TimelineMode.live, drags: _failingDragAmount, - captureStart: - isGlobal ? shared.timelineHeader : '馃敶 - Recording live timeline', + captureStart: [ + if (isGlobal) shared.timelineHeader else '馃敶 - Recording live timeline' + ], ); final lines = stdout.split('\n'); @@ -290,7 +291,7 @@ class ActDragTimelineTestBodies { static Future _outputFromDragTestProcess({ required String title, required TimelineMode timelineMode, - String captureStart = shared.timelineHeader, + List captureStart = const [shared.timelineHeader], bool isGlobalMode = false, TimelineMode? globalTimelineModeToSwitch, required int drags, diff --git a/test/timeline/tap/act_tap_timeline_test_bodies.dart b/test/timeline/tap/act_tap_timeline_test_bodies.dart index d2d9f598..6e7466ee 100644 --- a/test/timeline/tap/act_tap_timeline_test_bodies.dart +++ b/test/timeline/tap/act_tap_timeline_test_bodies.dart @@ -53,7 +53,7 @@ class ActTapTimelineTestBodies { timelineMode: initialGlobalMode, shouldFail: true, isGlobalMode: isGlobalMode, - captureStart: 'The following StateError was thrown running a test:', + captureStart: ['The following StateError was thrown running a test:'], globalTimelineModeToSwitch: globalTimelineModeToSwitch, ); final expectedErrorMessage = ''' @@ -72,7 +72,7 @@ Example: timeline.mode = $globalTimelineModeToSwitch; timelineMode: TimelineMode.record, shouldFail: true, isGlobalMode: isGlobalMode, - captureStart: 'Timeline', + captureStart: ['Timeline'], ); final timeline = stdout.split('\n'); @@ -124,6 +124,7 @@ Example: timeline.mode = $globalTimelineModeToSwitch; title: 'Live timeline without error prints html', timelineMode: TimelineMode.live, isGlobalMode: isGlobalMode, + captureStart: ['Timeline', shared.timelineHeader], ); final timeline = stdout.split('\n'); @@ -228,8 +229,8 @@ Example: timeline.mode = $globalTimelineModeToSwitch; await act.tap(_subtractButtonSelector); spotText('Counter: 3').existsOnce(); }); - - expect(output, contains('鈴革笌 - Timeline recording is off')); + final showsMessage = output.contains('鈴革笌 - Timeline recording is off'); + expect(showsMessage, isGlobalMode ? isFalse : isTrue); expect( output, isNot(contains('Tap ${_addButtonSelector.toStringBreadcrumb()}')), @@ -335,10 +336,20 @@ Example: timeline.mode = $globalTimelineModeToSwitch; callerParts.length, eventCount, ); - expect( - RegExp('Screenshot: file:').allMatches(output).length, - eventCount, - ); + final screenshots = output.split('\n').where((line) { + return line.startsWith('Screenshot: file:'); + }).toList(); + if (WidgetsBinding.instance is! LiveTestWidgetsFlutterBinding) { + expect( + screenshots.length, + eventCount, + ); + } else { + expect( + screenshots.length, + 0, + ); + } expect( RegExp('Timestamp: ').allMatches(output).length, eventCount, @@ -395,7 +406,7 @@ void main() async { static Future _outputFromTapTestProcess({ required String title, required TimelineMode timelineMode, - String captureStart = shared.timelineHeader, + List captureStart = const [shared.timelineHeader], bool shouldFail = false, bool isGlobalMode = false, TimelineMode? globalTimelineModeToSwitch, diff --git a/test/util/run_test_in_process.dart b/test/util/run_test_in_process.dart index 3e1953e0..00780863 100644 --- a/test/util/run_test_in_process.dart +++ b/test/util/run_test_in_process.dart @@ -15,7 +15,7 @@ import 'package:test_process/test_process.dart'; /// output will be captured starting from the line that matches `captureStart`. Future runTestInProcessAndCaptureOutPut({ required String testAsString, - String? captureStart, + List captureStart = const [], bool shouldFail = false, Iterable? args, }) async { @@ -29,11 +29,11 @@ Future runTestInProcessAndCaptureOutPut({ final testProcess = await TestProcess.start('flutter', arguments); final stdoutBuffer = StringBuffer(); - bool write = captureStart == null; + bool write = captureStart.isEmpty; await for (final line in testProcess.stdoutStream()) { if (line.isEmpty) continue; - if (!write && line == captureStart) { + if (!write && captureStart.contains(line)) { write = true; } if (write) { From 2668d53c382857523234b963b9a79617fb50a6c7 Mon Sep 17 00:00:00 2001 From: danielmolnar Date: Wed, 24 Jul 2024 15:42:09 +0200 Subject: [PATCH 115/119] Fix format --- lib/src/act/act.dart | 4 ++-- test/timeline/drag/act_drag_timeline_test_bodies.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/act/act.dart b/lib/src/act/act.dart index face2277..16dbcf9c 100644 --- a/lib/src/act/act.dart +++ b/lib/src/act/act.dart @@ -86,7 +86,7 @@ class Act { final snapshot = selector.snapshot()..existsOnce(); return TestAsyncUtils.guard(() async { - return await _alwaysPropagateDevicePointerEvents(() async { + return _alwaysPropagateDevicePointerEvents(() async { final renderBox = _getRenderBoxOrThrow(selector); // Before tapping the widget, we need to make sure that the widget is @@ -171,7 +171,7 @@ class Act { final snapshot = dragStart.snapshot()..existsOnce(); return TestAsyncUtils.guard(() async { - return await _alwaysPropagateDevicePointerEvents(() async { + return _alwaysPropagateDevicePointerEvents(() async { final renderBox = _getRenderBoxOrThrow(dragStart); final binding = TestWidgetsFlutterBinding.instance; diff --git a/test/timeline/drag/act_drag_timeline_test_bodies.dart b/test/timeline/drag/act_drag_timeline_test_bodies.dart index e1e9b04a..ececce02 100644 --- a/test/timeline/drag/act_drag_timeline_test_bodies.dart +++ b/test/timeline/drag/act_drag_timeline_test_bodies.dart @@ -163,7 +163,7 @@ class ActDragTimelineTestBodies { timelineMode: TimelineMode.live, drags: _failingDragAmount, captureStart: [ - if (isGlobal) shared.timelineHeader else '馃敶 - Recording live timeline' + if (isGlobal) shared.timelineHeader else '馃敶 - Recording live timeline', ], ); From 48b5eb3d2a9d3aa2d08bc1163299d81524c7340b Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Wed, 24 Jul 2024 16:36:28 +0200 Subject: [PATCH 116/119] Use flutter executable that started the test --- test/util/run_test_in_process.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/util/run_test_in_process.dart b/test/util/run_test_in_process.dart index 00780863..d2d7af67 100644 --- a/test/util/run_test_in_process.dart +++ b/test/util/run_test_in_process.dart @@ -27,7 +27,14 @@ Future runTestInProcessAndCaptureOutPut({ ...?args?.where((arg) => arg != 'test'), ]; - final testProcess = await TestProcess.start('flutter', arguments); + // Get the path to the Flutter executable the test was started with (not from PATH) + // /Users/pascalwelsch/.puro/envs/3.16.9/flutter/bin/cache/artifacts/engine/darwin-x64/flutter_tester + final flutterTesterExe = Platform.executable; + final binDir = flutterTesterExe.split('/cache/')[0]; + final flutterExe = + Platform.isWindows ? '$binDir\\flutter.exe' : '$binDir/flutter'; + + final testProcess = await TestProcess.start(flutterExe, arguments); final stdoutBuffer = StringBuffer(); bool write = captureStart.isEmpty; From d2b83af0b9aaec9053ca32cc1fa0ac421a41efc2 Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Wed, 24 Jul 2024 16:44:56 +0200 Subject: [PATCH 117/119] Fix typos --- lib/src/spot/matcher_generator.dart | 2 +- lib/src/timeline/timeline.dart | 2 +- test/spot/screenshot_test.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/spot/matcher_generator.dart b/lib/src/spot/matcher_generator.dart index 35c3c703..1f663eba 100644 --- a/lib/src/spot/matcher_generator.dart +++ b/lib/src/spot/matcher_generator.dart @@ -128,7 +128,7 @@ extension ${widgetType}Getter on WidgetMatcher<$widgetType> { continue; } if (prop.name == 'depth' || prop.name == 'key') { - // ignore default properties that are covered by general Wiget selectors + // ignore default properties that are covered by general Widget selectors continue; } if (prop.name == 'dependencies') { diff --git a/lib/src/timeline/timeline.dart b/lib/src/timeline/timeline.dart index 5ac9565e..241be50c 100644 --- a/lib/src/timeline/timeline.dart +++ b/lib/src/timeline/timeline.dart @@ -45,7 +45,7 @@ Example: timeline.mode = $value; _globalTimelineMode = value; } -/// Use --dart-define=SPOT_TIMELINE_MODE=live|record|off to set the [TimlineMode] +/// Use --dart-define=SPOT_TIMELINE_MODE=live|record|off to set the [TimelineMode] /// for all tests TimelineMode? getTimelineModeFromEnv() { final mode = const String.fromEnvironment('SPOT_TIMELINE_MODE').toLowerCase(); diff --git a/test/spot/screenshot_test.dart b/test/spot/screenshot_test.dart index fec750a7..b629a90f 100644 --- a/test/spot/screenshot_test.dart +++ b/test/spot/screenshot_test.dart @@ -264,7 +264,7 @@ void main() { ); }); - group('Annotage Screenshot test', () { + group('Annotate Screenshot test', () { testWidgets('Take screenshot with tap marker of the entire app', (tester) async { tester.view.physicalSize = const Size(210, 210); From a9c06989d52f31fba3e460f1b41721ea05de910e Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Wed, 24 Jul 2024 16:48:41 +0200 Subject: [PATCH 118/119] Remove unused globalTimelineModeToSwitch --- test/timeline/drag/act_drag_timeline_test_bodies.dart | 8 -------- test/timeline/tap/act_tap_timeline_test_bodies.dart | 8 ++++---- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/test/timeline/drag/act_drag_timeline_test_bodies.dart b/test/timeline/drag/act_drag_timeline_test_bodies.dart index ececce02..277fc8f7 100644 --- a/test/timeline/drag/act_drag_timeline_test_bodies.dart +++ b/test/timeline/drag/act_drag_timeline_test_bodies.dart @@ -300,7 +300,6 @@ class ActDragTimelineTestBodies { title: title, timelineMode: timelineMode, isGlobalMode: isGlobalMode, - globalTimelineModeToSwitch: globalTimelineModeToSwitch, drags: drags, ); @@ -317,13 +316,7 @@ class ActDragTimelineTestBodies { required TimelineMode timelineMode, required int drags, bool isGlobalMode = false, - TimelineMode? globalTimelineModeToSwitch, }) { - final switchPart = globalTimelineModeToSwitch != null - ? ''' - globalTimelineMode = TimelineMode.${globalTimelineModeToSwitch.toString().split('.').last}; - ''' - : ''; final testTitle = '${isGlobalMode ? 'Global: ' : 'Local: '}$title'; final globalInitiator = @@ -343,7 +336,6 @@ void main() async { $globalInitiator testWidgets("$testTitle", (WidgetTester tester) async { $localInitiator - $switchPart await tester.pumpWidget(const DragUntilVisibleTestWidget()); final firstItem = spotText('Item at index: 3', exact: true)..existsOnce(); final secondItem = spotText('Item at index: 27', exact: true) diff --git a/test/timeline/tap/act_tap_timeline_test_bodies.dart b/test/timeline/tap/act_tap_timeline_test_bodies.dart index 6e7466ee..83b51e87 100644 --- a/test/timeline/tap/act_tap_timeline_test_bodies.dart +++ b/test/timeline/tap/act_tap_timeline_test_bodies.dart @@ -128,7 +128,7 @@ Example: timeline.mode = $globalTimelineModeToSwitch; ); final timeline = stdout.split('\n'); -// Does not start with 'Timeline', this only happens on error + // Does not start with 'Timeline', this only happens on error expect(timeline.first, shared.timelineHeader); expect( timeline.second, @@ -176,7 +176,7 @@ Example: timeline.mode = $globalTimelineModeToSwitch; ); final timeline = stdout.split('\n'); -// Does not start with 'Timeline', this only happens on error + // Does not start with 'Timeline', this only happens on error expect(timeline.first, shared.timelineHeader); expect( timeline.second, @@ -259,7 +259,7 @@ Example: timeline.mode = $globalTimelineModeToSwitch; spotText('Counter: 4').existsOnce(); await act.tap(_subtractButtonSelector); spotText('Counter: 3').existsOnce(); -// Notify that the recording stopped + // Notify that the recording stopped timeline.mode = TimelineMode.off; await act.tap(_clearButtonSelector); spotText('Counter: 0').existsOnce(); @@ -277,7 +277,7 @@ Example: timeline.mode = $globalTimelineModeToSwitch; output, contains('Tap ${_subtractButtonSelector.toStringBreadcrumb()}'), ); -// No further events were added to the timeline, including screenshots + // No further events were added to the timeline, including screenshots expect( output, isNot(contains('Tap ${_clearButtonSelector.toStringBreadcrumb()}')), From 817f3b5c37876399b2ddbec72d331a0e898ac9ba Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Tue, 13 Aug 2024 14:46:37 +0200 Subject: [PATCH 119/119] Add pixelRatio to screenshot --- lib/src/screenshot/screenshot.dart | 99 ++++++++++++++++++++++++------ test/spot/screenshot_test.dart | 40 +++++++++++- 2 files changed, 118 insertions(+), 21 deletions(-) diff --git a/lib/src/screenshot/screenshot.dart b/lib/src/screenshot/screenshot.dart index 3cd898db..8ada48e7 100644 --- a/lib/src/screenshot/screenshot.dart +++ b/lib/src/screenshot/screenshot.dart @@ -1,5 +1,5 @@ import 'dart:core'; -import 'dart:core' as core; +// import 'dart:core' as core; import 'dart:io'; import 'dart:typed_data'; import 'dart:ui' as ui; @@ -17,14 +17,18 @@ import 'package:stack_trace/stack_trace.dart'; export 'package:stack_trace/stack_trace.dart' show Frame; -/// A screenshot taken from a widget test. +/// A screenshot taken from a widget /// /// May also be just a single widget, not the entire screen class Screenshot { /// Creates a [Screenshot] that points to a file on disk. Screenshot({ required this.file, + required this.pixels, this.initiator, + required this.width, + required this.height, + required this.pixelRatio, }); /// The file where the screenshot was saved to @@ -32,6 +36,45 @@ class Screenshot { /// Call stack of the code that initiated the screenshot final Frame? initiator; + + /// The image data of the screenshot in RGBA format + final Uint8List pixels; + + /// The width of the screenshot logical pixels + /// + /// width * pixelRatio = physical pixels + final int width; + + /// The height of the screenshot in logical pixels + /// + /// height * pixelRatio = physical pixels + final int height; + + /// The pixel ratio of the screenshot + final double pixelRatio; + + /// The width of the screenshot in physical pixels + int get physicalPixelWidth => (width * pixelRatio).round(); + + /// The height of the screenshot in physical pixels + int get physicalPixelHeight => (height * pixelRatio).round(); + + /// The image data + Future get image async { + final buffer = await ui.ImmutableBuffer.fromUint8List(pixels); + final descriptor = ui.ImageDescriptor.raw( + buffer, + width: width, + height: height, + pixelFormat: ui.PixelFormat.rgba8888, + ); + final codec = await descriptor.instantiateCodec(); + codec.dispose(); + final frame = await codec.getNextFrame(); + final image = frame.image; + image.dispose(); + return image; + } } /// Takes a screenshot of the entire screen or a single widget. @@ -150,19 +193,26 @@ Future _createScreenshot({ return binding.renderViewElement!; }(); - late final Uint8List image; - await binding.runAsync(() async { - final plainImage = await _captureImage(liveElement); - final ui.Image imageToCapture = - await annotator?.annotate(plainImage) ?? plainImage; - final byteData = - await imageToCapture.toByteData(format: ui.ImageByteFormat.png); - if (byteData == null) { - throw 'Could not take screenshot'; - } - image = byteData.buffer.asUint8List(); - plainImage.dispose(); + final view = binding.platformDispatcher.implicitView; + final devicePixelRatio = view?.devicePixelRatio ?? 1.0; + + ui.Image? image = await binding.runAsync(() async { + return await _captureImage(liveElement, devicePixelRatio); + }); + if (image == null) { + throw 'Could not take screenshot'; + } + if (annotator != null) { + image = await binding.runAsync(() => annotator.annotate(image!)); + } + final byteData = await binding.runAsync(() async { + return await image!.toByteData(format: ui.ImageByteFormat.png); }); + if (byteData == null) { + throw 'Could not take screenshot'; + } + final Uint8List imageBytes = byteData.buffer.asUint8List(); + image!.dispose(); final spotTempDir = Directory.systemTemp.directory('spot'); if (!spotTempDir.existsSync()) { @@ -195,17 +245,23 @@ Future _createScreenshot({ }(); final file = spotTempDir.file(screenshotFileName); - file.writeAsBytesSync(image); + file.writeAsBytesSync(imageBytes); if (printToConsole) { // ignore: avoid_print - core.print( + print( 'Screenshot file://${file.path}\n' ' taken at ${frame?.member} ${frame?.uri}:${frame?.line}:${frame?.column}', ); } - - return Screenshot(file: file, initiator: frame); + return Screenshot( + file: file, + pixels: imageBytes, + initiator: frame, + width: image.width ~/ devicePixelRatio, + height: image.height ~/ devicePixelRatio, + pixelRatio: devicePixelRatio, + ); } /// Provides the ability to create screenshots of a [WidgetSelector] @@ -241,7 +297,7 @@ extension ElementScreenshotExtension on Element { /// See also: /// /// * [OffsetLayer.toImage] which is the actual method being called. -Future _captureImage(Element element) async { +Future _captureImage(Element element, double pixelRatio) async { assert(element.renderObject != null); RenderObject renderObject = element.renderObject!; while (!renderObject.isRepaintBoundary) { @@ -251,7 +307,10 @@ Future _captureImage(Element element) async { assert(!renderObject.debugNeedsPaint); final OffsetLayer layer = renderObject.debugLayer! as OffsetLayer; - final ui.Image image = await layer.toImage(renderObject.paintBounds); + final ui.Image image = await layer.toImage( + renderObject.paintBounds, + pixelRatio: pixelRatio, + ); if (element.renderObject is RenderBox) { final expectedSize = (element.renderObject as RenderBox?)!.size; diff --git a/test/spot/screenshot_test.dart b/test/spot/screenshot_test.dart index b629a90f..6e16fcad 100644 --- a/test/spot/screenshot_test.dart +++ b/test/spot/screenshot_test.dart @@ -14,7 +14,7 @@ import '../util/assert_error.dart'; void main() { testWidgets('Take screenshot of the entire app', (tester) async { tester.view.physicalSize = const Size(210, 210); - tester.view.devicePixelRatio = 1.0; + tester.view.devicePixelRatio = 3.0; const red = Color(0xffff0000); await tester.pumpWidget( Center( @@ -24,11 +24,49 @@ void main() { final shot = await takeScreenshot(); expect(shot.file.existsSync(), isTrue); + expect(shot.width, 210); + expect(shot.height, 210); + expect(shot.physicalPixelHeight, 630); + expect(shot.physicalPixelWidth, 630); + expect(shot.pixelRatio, 3.0); + expect(shot.pixels, isNotNull); final redPixelCoverage = await percentageOfPixelsWithColor(shot.file, red); expect(redPixelCoverage, greaterThan(0.9)); }); + testWidgets('Accounts for devicePixelRatio', (tester) async { + tester.view.physicalSize = const Size(210, 210); + await tester.pumpWidget( + Center( + child: Container(height: 200, width: 200, color: Color(0xffff0000)), + ), + ); + + addTearDown(() => tester.view.resetDevicePixelRatio()); + tester.view.devicePixelRatio = 1.0; + await tester.pump(); + { + final shot = await takeScreenshot(); + expect(shot.file.existsSync(), isTrue); + final pixels = shot.file.readAsBytesSync(); + final image = img.decodeImage(pixels)!; + expect(image.width, 210); + expect(image.width, 210); + } + + tester.view.devicePixelRatio = 2.0; + await tester.pump(); + { + final shot = await takeScreenshot(); + expect(shot.file.existsSync(), isTrue); + final pixels = shot.file.readAsBytesSync(); + final image = img.decodeImage(pixels)!; + expect(image.width, 420); + expect(image.width, 420); + } + }); + testWidgets('Take screenshot from a selector', (tester) async { tester.view.physicalSize = const Size(1000, 1000); tester.view.devicePixelRatio = 1.0;