diff --git a/lib/src/components/organisms/selectable_widget.dart b/lib/src/components/organisms/selectable_widget.dart index 8d5300f..9e6c5a5 100644 --- a/lib/src/components/organisms/selectable_widget.dart +++ b/lib/src/components/organisms/selectable_widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -42,6 +44,7 @@ class ZdsSelectableWidget extends StatefulWidget { class _ZdsSelectableWidgetState extends State { bool isSelected = false; + Timer? _selectionTimer; void toggleSelection() { setState(() { @@ -54,6 +57,12 @@ class _ZdsSelectableWidgetState extends State { return document.body?.text ?? ''; } + @override + void dispose() { + _selectionTimer?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { if (!(widget.copyable ?? false)) return widget.child; @@ -61,6 +70,7 @@ class _ZdsSelectableWidgetState extends State { return GestureDetector( child: ColoredBox(color: isSelected ? zeta.primary.surface : Colors.transparent, child: widget.child), onLongPress: () async { + if (isSelected) return; toggleSelection(); if (isSelected) { var copiedText = widget.textToCopy; @@ -88,8 +98,8 @@ class _ZdsSelectableWidgetState extends State { ), ), ); + _selectionTimer = Timer(const Duration(seconds: 4), toggleSelection); } - Future.delayed(const Duration(seconds: 4), toggleSelection); }, ); } diff --git a/test/fixtures/test_app.dart b/test/fixtures/test_app.dart new file mode 100644 index 0000000..ef72249 --- /dev/null +++ b/test/fixtures/test_app.dart @@ -0,0 +1,31 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class TestApp extends StatelessWidget { + const TestApp({super.key, required this.builder}); + final WidgetBuilder builder; + @override + Widget build(BuildContext context) { + return MaterialApp( + localizationsDelegates: >[ + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ComponentDelegate(testing: true), + ], + home: ZetaProvider( + builder: (context, themeData, themeMode) { + return Scaffold(body: builder.call(context)); + }, + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ObjectFlagProperty.has('builder', builder)); + } +} diff --git a/test/lib/src/components/organisms/selectable_widget_test.dart b/test/lib/src/components/organisms/selectable_widget_test.dart new file mode 100644 index 0000000..4d28a8a --- /dev/null +++ b/test/lib/src/components/organisms/selectable_widget_test.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:zds_flutter/zds_flutter.dart'; +import '../../../../fixtures/test_app.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + var clipboardContent = 'Selectable Text'; + setUp(() { + // Mock the clipboard data + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, + (MethodCall methodCall) async { + if (methodCall.method == 'Clipboard.getData') { + return {'text': clipboardContent}; + } + return null; + }); + }); + tearDown(() { + // Remove the mock handler + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, null); + }); + group('ZdsSelectableWidget Tests', () { + testWidgets('Displays child widget', (WidgetTester tester) async { + const childWidget = Text('Selectable Text'); + clipboardContent = 'Selectable Text'; + await tester.pumpWidget( + TestApp( + builder: (BuildContext context) { + return const ZdsSelectableWidget( + textToCopy: 'Selectable Text', + child: childWidget, + ); + }, + ), + ); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.byWidget(childWidget), findsOneWidget); + }); + testWidgets('Copies plain text to clipboard on long press', (WidgetTester tester) async { + const childWidget = Text('Selectable Text'); + clipboardContent = 'Selectable Text'; + await tester.pumpWidget( + TestApp( + builder: (BuildContext context) { + return const ZdsSelectableWidget( + textToCopy: 'Selectable Text', + copyable: true, + child: childWidget, + ); + }, + ), + ); + await tester.pump(const Duration(milliseconds: 100)); + await tester.longPress(find.byWidget(childWidget)); + await tester.pumpAndSettle(); + final clipboardData = await Clipboard.getData('text/plain'); + expect(clipboardData?.text, clipboardContent); + }); + testWidgets('Copies HTML text to clipboard as plain text on long press', (WidgetTester tester) async { + const childWidget = Text('Selectable HTML Text'); + const htmlText = '

Selectable HTML Text

'; + clipboardContent = 'Selectable HTML Text'; + await tester.pumpWidget( + TestApp( + builder: (BuildContext context) { + return const ZdsSelectableWidget( + textToCopy: htmlText, + isHtmlData: true, + copyable: true, + child: childWidget, + ); + }, + ), + ); + await tester.pump(const Duration(milliseconds: 100)); + await tester.longPress(find.byWidget(childWidget)); + await tester.pumpAndSettle(); + final clipboardData = await Clipboard.getData('text/plain'); + expect(clipboardData?.text, 'Selectable HTML Text'); + }); + testWidgets('Does not copy text if copyable is false', (WidgetTester tester) async { + const childWidget = Text('Non-copyable Text'); + clipboardContent = ''; + await tester.pumpWidget( + TestApp( + builder: (BuildContext context) { + return const ZdsSelectableWidget( + textToCopy: 'Non-copyable Text', + copyable: false, + child: childWidget, + ); + }, + ), + ); + await tester.pump(const Duration(milliseconds: 100)); + await tester.longPress(find.byWidget(childWidget)); + await tester.pumpAndSettle(); + final clipboardData = await Clipboard.getData('text/plain'); + expect(clipboardData?.text, ''); + }); + }); +}