From 0b3f2bbefb07c8ea84b19866c82358312e97b830 Mon Sep 17 00:00:00 2001 From: preetidas60 Date: Mon, 20 Oct 2025 12:45:33 +0530 Subject: [PATCH 1/4] Implemented Comprehensive Testing Suite --- lib/models/draw_model.dart | 70 ++-- lib/tests/canvas_cubit_test.dart | 565 ++++++++++++++++++++++++++++ lib/tests/draw_model_test.dart | 436 +++++++++++++++++++++ lib/tests/text_item_model_test.dart | 332 ++++++++++++++++ lib/tests/widgets_test.dart | 558 +++++++++++++++++++++++++++ pubspec.yaml | 1 + test/widget_test.dart | 54 ++- 7 files changed, 1971 insertions(+), 45 deletions(-) create mode 100644 lib/tests/canvas_cubit_test.dart create mode 100644 lib/tests/draw_model_test.dart create mode 100644 lib/tests/text_item_model_test.dart create mode 100644 lib/tests/widgets_test.dart diff --git a/lib/models/draw_model.dart b/lib/models/draw_model.dart index 4a44240b..36a5e2af 100644 --- a/lib/models/draw_model.dart +++ b/lib/models/draw_model.dart @@ -15,6 +15,35 @@ class DrawingPoint { required this.paint, this.pressure = 1.0, }); + + Map toJson() { + return { + 'offsetX': offset.dx, + 'offsetY': offset.dy, + 'color': paint.color.value, + 'strokeWidth': paint.strokeWidth, + 'strokeCap': paint.strokeCap.index, + 'strokeJoin': paint.strokeJoin.index, + 'pressure': pressure, + }; + } + + factory DrawingPoint.fromJson(Map json) { + final paint = Paint() + ..color = Color(json['color'] as int) + ..strokeWidth = (json['strokeWidth'] as num).toDouble() + ..strokeCap = StrokeCap.values[json['strokeCap'] as int] + ..strokeJoin = StrokeJoin.values[json['strokeJoin'] as int]; + + return DrawingPoint( + offset: Offset( + (json['offsetX'] as num).toDouble(), + (json['offsetY'] as num).toDouble(), + ), + paint: paint, + pressure: (json['pressure'] as num?)?.toDouble() ?? 1.0, + ); + } } class DrawPath { @@ -52,17 +81,8 @@ class DrawPath { Map toJson() { return { - 'points': points - .map((point) => { - 'x': point.offset.dx, - 'y': point.offset.dy, - 'color': point.paint.color.toARGB32(), - 'strokeWidth': point.paint.strokeWidth, - 'pressure': point.pressure, - 'style': point.paint.style.index, // Save paint style - }) - .toList(), - 'color': color.toARGB32(), + 'points': points.map((point) => point.toJson()).toList(), + 'color': color.value, 'strokeWidth': strokeWidth, 'strokeCap': strokeCap.index, 'isFill': isFill, @@ -74,24 +94,18 @@ class DrawPath { final List pointsList = json['points']; return DrawPath( - points: pointsList.map((pointJson) { - return DrawingPoint( - offset: Offset(pointJson['x'], pointJson['y']), - paint: Paint() - ..color = Color(pointJson['color']) - ..strokeWidth = pointJson['strokeWidth'] - ..strokeCap = StrokeCap.round - ..style = pointJson['style'] != null - ? PaintingStyle.values[pointJson['style']] - : PaintingStyle.stroke, // Default to stroke for compatibility - pressure: pointJson['pressure'] ?? 1.0, - ); - }).toList(), - color: Color(json['color']), - strokeWidth: json['strokeWidth'], - strokeCap: StrokeCap.values[json['strokeCap']], + points: pointsList + .map((pointJson) => DrawingPoint.fromJson(pointJson)) + .toList(), + color: Color(json['color'] as int), + strokeWidth: (json['strokeWidth'] as num).toDouble(), + strokeCap: json['strokeCap'] != null + ? StrokeCap.values[json['strokeCap'] as int] + : StrokeCap.round, isFill: json['isFill'] ?? false, - brushType: BrushType.values[json['brushType'] ?? 0], + brushType: json['brushType'] != null + ? BrushType.values[json['brushType'] as int] + : BrushType.brush, ); } } diff --git a/lib/tests/canvas_cubit_test.dart b/lib/tests/canvas_cubit_test.dart new file mode 100644 index 00000000..516cc584 --- /dev/null +++ b/lib/tests/canvas_cubit_test.dart @@ -0,0 +1,565 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:bloc_test/bloc_test.dart'; +import 'package:texterra/cubit/canvas_cubit.dart'; +import 'package:texterra/cubit/canvas_state.dart'; +import 'package:texterra/models/text_item_model.dart'; +import 'package:texterra/models/draw_model.dart'; +import 'package:texterra/constants/color_constants.dart'; + +void main() { + // Initialize Flutter binding before running tests + TestWidgetsFlutterBinding.ensureInitialized(); + + group('CanvasCubit', () { + late CanvasCubit canvasCubit; + + setUp(() { + canvasCubit = CanvasCubit(); + }); + + tearDown(() { + canvasCubit.close(); + }); + + test('initial state is correct', () { + expect(canvasCubit.state.textItems, isEmpty); + expect(canvasCubit.state.drawPaths, isEmpty); + expect( + canvasCubit.state.backgroundColor, ColorConstants.backgroundDarkGray); + expect(canvasCubit.state.selectedTextItemIndex, isNull); + expect(canvasCubit.state.isDrawingMode, false); + expect(canvasCubit.state.isTrayShown, false); + }); + + group('Text Operations', () { + blocTest( + 'addText adds a new text item', + build: () => canvasCubit, + act: (cubit) => cubit.addText('Hello World'), + expect: () => [ + predicate((state) { + return state.textItems.length == 1 && + state.textItems.first.text == 'Hello World' && + state.selectedTextItemIndex == 0; + }), + ], + ); + + blocTest( + 'addText creates items with offset positions', + build: () => canvasCubit, + act: (cubit) { + cubit.addText('First'); + cubit.addText('Second'); + }, + verify: (cubit) { + expect(cubit.state.textItems.length, 2); + expect(cubit.state.textItems[0].x, 50); + expect(cubit.state.textItems[0].y, 50); + expect(cubit.state.textItems[1].x, 70); // 50 + 20 offset + expect(cubit.state.textItems[1].y, 70); // 50 + 20 offset + }, + ); + + blocTest( + 'editText updates text content', + build: () => canvasCubit, + seed: () { + canvasCubit.addText('Original'); + return canvasCubit.state; + }, + act: (cubit) => cubit.editText(0, 'Updated'), + verify: (cubit) { + expect(cubit.state.textItems.first.text, 'Updated'); + }, + ); + + blocTest( + 'deleteText removes text item', + build: () => canvasCubit, + seed: () { + canvasCubit.addText('Test'); + return canvasCubit.state; + }, + act: (cubit) => cubit.deleteText(0), + verify: (cubit) { + expect(cubit.state.textItems, isEmpty); + expect(cubit.state.selectedTextItemIndex, isNull); + }, + ); + + blocTest( + 'selectText updates selected index', + build: () => canvasCubit, + seed: () { + canvasCubit.addText('Test'); + return canvasCubit.state; + }, + act: (cubit) => cubit.selectText(0), + verify: (cubit) { + expect(cubit.state.selectedTextItemIndex, 0); + }, + ); + + blocTest( + 'deselectText clears selection', + build: () => canvasCubit, + seed: () { + canvasCubit.addText('Test'); + canvasCubit.selectText(0); + return canvasCubit.state; + }, + act: (cubit) => cubit.deselectText(), + verify: (cubit) { + expect(cubit.state.selectedTextItemIndex, isNull); + }, + ); + + blocTest( + 'moveText updates position', + build: () => canvasCubit, + seed: () { + canvasCubit.addText('Test'); + return canvasCubit.state; + }, + act: (cubit) => cubit.moveText(0, 100, 200), + verify: (cubit) { + expect(cubit.state.textItems.first.x, 100); + expect(cubit.state.textItems.first.y, 200); + }, + ); + }); + + group('Font Styling', () { + blocTest( + 'changeFontSize updates font size', + build: () => canvasCubit, + seed: () { + canvasCubit.addText('Test'); + return canvasCubit.state; + }, + act: (cubit) => cubit.changeFontSize(0, 24.0), + verify: (cubit) { + expect(cubit.state.textItems.first.fontSize, 24.0); + }, + ); + + blocTest( + 'changeFontFamily updates font family', + build: () => canvasCubit, + seed: () { + canvasCubit.addText('Test'); + return canvasCubit.state; + }, + act: (cubit) => cubit.changeFontFamily(0, 'Arial'), + verify: (cubit) { + expect(cubit.state.textItems.first.fontFamily, 'Arial'); + }, + ); + + blocTest( + 'changeFontWeight updates font weight', + build: () => canvasCubit, + seed: () { + canvasCubit.addText('Test'); + return canvasCubit.state; + }, + act: (cubit) => cubit.changeFontWeight(0, FontWeight.bold), + verify: (cubit) { + expect(cubit.state.textItems.first.fontWeight, FontWeight.bold); + }, + ); + + blocTest( + 'changeFontStyle updates font style', + build: () => canvasCubit, + seed: () { + canvasCubit.addText('Test'); + return canvasCubit.state; + }, + act: (cubit) => cubit.changeFontStyle(0, FontStyle.italic), + verify: (cubit) { + expect(cubit.state.textItems.first.fontStyle, FontStyle.italic); + }, + ); + + blocTest( + 'changeTextUnderline toggles underline', + build: () => canvasCubit, + seed: () { + canvasCubit.addText('Test'); + return canvasCubit.state; + }, + act: (cubit) => cubit.changeTextUnderline(0, true), + verify: (cubit) { + expect(cubit.state.textItems.first.isUnderlined, true); + }, + ); + + blocTest( + 'changeTextColor updates color', + build: () => canvasCubit, + seed: () { + canvasCubit.addText('Test'); + return canvasCubit.state; + }, + act: (cubit) => cubit.changeTextColor(0, Colors.red), + verify: (cubit) { + expect(cubit.state.textItems.first.color, Colors.red); + }, + ); + + blocTest( + 'changeTextAlignment updates text alignment', + build: () => canvasCubit, + seed: () { + canvasCubit.addText('Test'); + return canvasCubit.state; + }, + act: (cubit) => cubit.changeTextAlignment(0, TextAlign.center), + verify: (cubit) { + expect(cubit.state.textItems.first.textAlign, TextAlign.center); + }, + ); + }); + + group('Text Highlighting', () { + blocTest( + 'toggleTextHighlight enables highlighting', + build: () => canvasCubit, + seed: () { + canvasCubit.addText('Test'); + return canvasCubit.state; + }, + act: (cubit) => cubit.toggleTextHighlight(0), + verify: (cubit) { + expect(cubit.state.textItems.first.isHighlighted, true); + expect(cubit.state.textItems.first.highlightColor, + ColorConstants.highlightYellow); + }, + ); + + blocTest( + 'changeHighlightColor updates highlight color', + build: () => canvasCubit, + seed: () { + canvasCubit.addText('Test'); + return canvasCubit.state; + }, + act: (cubit) => cubit.changeHighlightColor(0, Colors.lime), + verify: (cubit) { + expect(cubit.state.textItems.first.isHighlighted, true); + expect(cubit.state.textItems.first.highlightColor, Colors.lime); + }, + ); + }); + + group('Text Shadow', () { + blocTest( + 'toggleTextShadow enables shadow', + build: () => canvasCubit, + seed: () { + canvasCubit.addText('Test'); + return canvasCubit.state; + }, + act: (cubit) => cubit.toggleTextShadow(0), + verify: (cubit) { + expect(cubit.state.textItems.first.hasShadow, true); + }, + ); + + blocTest( + 'changeShadowColor updates shadow color', + build: () => canvasCubit, + seed: () { + canvasCubit.addText('Test'); + return canvasCubit.state; + }, + act: (cubit) => cubit.changeShadowColor(0, Colors.blue), + verify: (cubit) { + expect(cubit.state.textItems.first.hasShadow, true); + expect(cubit.state.textItems.first.shadowColor, Colors.blue); + }, + ); + + blocTest( + 'changeShadowBlur updates blur radius', + build: () => canvasCubit, + seed: () { + canvasCubit.addText('Test'); + return canvasCubit.state; + }, + act: (cubit) => cubit.changeShadowBlur(0, 10.0), + verify: (cubit) { + expect(cubit.state.textItems.first.shadowBlurRadius, 10.0); + }, + ); + + blocTest( + 'changeShadowOffset updates offset', + build: () => canvasCubit, + seed: () { + canvasCubit.addText('Test'); + return canvasCubit.state; + }, + act: (cubit) => cubit.changeShadowOffset(0, const Offset(5, 5)), + verify: (cubit) { + expect(cubit.state.textItems.first.shadowOffset, const Offset(5, 5)); + }, + ); + + blocTest( + 'applyShadowPreset applies soft shadow', + build: () => canvasCubit, + seed: () { + canvasCubit.addText('Test'); + return canvasCubit.state; + }, + act: (cubit) => cubit.applyShadowPreset(0, ShadowPreset.soft), + verify: (cubit) { + final item = cubit.state.textItems.first; + expect(item.hasShadow, true); + expect(item.shadowBlurRadius, 8.0); + }, + ); + }); + + group('Format Reset', () { + blocTest( + 'resetFormatting resets all formatting', + build: () => canvasCubit, + seed: () { + canvasCubit.addText('Test'); + canvasCubit.changeFontSize(0, 32); + canvasCubit.changeFontWeight(0, FontWeight.bold); + canvasCubit.changeTextColor(0, Colors.red); + canvasCubit.toggleTextHighlight(0); + canvasCubit.toggleTextShadow(0); + return canvasCubit.state; + }, + act: (cubit) => cubit.resetFormatting(0), + verify: (cubit) { + final item = cubit.state.textItems.first; + expect(item.fontSize, 16); + expect(item.fontWeight, FontWeight.normal); + expect(item.fontStyle, FontStyle.normal); + expect(item.fontFamily, 'Roboto'); + expect(item.isUnderlined, false); + expect(item.isHighlighted, false); + expect(item.hasShadow, false); + expect(item.color, ColorConstants.uiWhite); + }, + ); + }); + + group('Background Operations', () { + blocTest( + 'changeBackgroundColor updates background', + build: () => canvasCubit, + act: (cubit) => cubit.changeBackgroundColor(Colors.blue), + verify: (cubit) { + expect(cubit.state.backgroundColor, Colors.blue); + }, + ); + + blocTest( + 'toggleTray toggles tray visibility', + build: () => canvasCubit, + act: (cubit) => cubit.toggleTray(), + verify: (cubit) { + expect(cubit.state.isTrayShown, true); + }, + ); + }); + + group('Drawing Mode', () { + blocTest( + 'toggleDrawingMode enables drawing mode', + build: () => canvasCubit, + act: (cubit) => cubit.toggleDrawingMode(), + verify: (cubit) { + expect(cubit.state.isDrawingMode, true); + expect(cubit.state.selectedTextItemIndex, isNull); + }, + ); + + blocTest( + 'setDrawingMode sets drawing mode explicitly', + build: () => canvasCubit, + act: (cubit) => cubit.setDrawingMode(true), + verify: (cubit) { + expect(cubit.state.isDrawingMode, true); + }, + ); + + blocTest( + 'setDrawColor updates drawing color', + build: () => canvasCubit, + act: (cubit) => cubit.setDrawColor(Colors.red), + verify: (cubit) { + expect(cubit.state.currentDrawColor, Colors.red); + }, + ); + + blocTest( + 'setStrokeWidth updates stroke width', + build: () => canvasCubit, + act: (cubit) => cubit.setStrokeWidth(10.0), + verify: (cubit) { + expect(cubit.state.currentStrokeWidth, 10.0); + }, + ); + + blocTest( + 'setBrushType updates brush type', + build: () => canvasCubit, + act: (cubit) => cubit.setBrushType(BrushType.marker), + verify: (cubit) { + expect(cubit.state.currentBrushType, BrushType.marker); + }, + ); + + blocTest( + 'startNewDrawPath creates new drawing path', + build: () => canvasCubit, + seed: () { + canvasCubit.setDrawingMode(true); + return canvasCubit.state; + }, + act: (cubit) => cubit.startNewDrawPath(const Offset(10, 10)), + verify: (cubit) { + expect(cubit.state.drawPaths.length, 1); + expect(cubit.state.drawPaths.first.points.length, 1); + }, + ); + + blocTest( + 'updateDrawPath adds points to current path', + build: () => canvasCubit, + seed: () { + canvasCubit.setDrawingMode(true); + canvasCubit.startNewDrawPath(const Offset(10, 10)); + return canvasCubit.state; + }, + act: (cubit) { + cubit.updateDrawPath(const Offset(20, 20)); + cubit.updateDrawPath(const Offset(30, 30)); + }, + verify: (cubit) { + expect(cubit.state.drawPaths.length, 1); + expect(cubit.state.drawPaths.first.points.length, 3); + }, + ); + + blocTest( + 'clearDrawings removes all drawing paths', + build: () => canvasCubit, + seed: () { + canvasCubit.setDrawingMode(true); + canvasCubit.startNewDrawPath(const Offset(10, 10)); + return canvasCubit.state; + }, + act: (cubit) => cubit.clearDrawings(), + verify: (cubit) { + expect(cubit.state.drawPaths, isEmpty); + }, + ); + + blocTest( + 'undoLastDrawing removes last path', + build: () => canvasCubit, + seed: () { + canvasCubit.setDrawingMode(true); + canvasCubit.startNewDrawPath(const Offset(10, 10)); + canvasCubit.startNewDrawPath(const Offset(20, 20)); + return canvasCubit.state; + }, + act: (cubit) => cubit.undoLastDrawing(), + verify: (cubit) { + expect(cubit.state.drawPaths.length, 1); + }, + ); + }); + + group('Canvas Operations', () { + blocTest( + 'clearCanvas removes all content', + build: () => canvasCubit, + seed: () { + canvasCubit.addText('Test'); + canvasCubit.setDrawingMode(true); + canvasCubit.startNewDrawPath(const Offset(10, 10)); + return canvasCubit.state; + }, + act: (cubit) => cubit.clearCanvas(), + verify: (cubit) { + expect(cubit.state.textItems, isEmpty); + expect(cubit.state.drawPaths, isEmpty); + expect(cubit.state.isDrawingMode, false); + expect(cubit.state.selectedTextItemIndex, isNull); + }, + ); + + blocTest( + 'createNewPage resets canvas state', + build: () => canvasCubit, + seed: () { + canvasCubit.addText('Test'); + return canvasCubit.state; + }, + act: (cubit) => cubit.createNewPage(), + verify: (cubit) { + expect(cubit.state.textItems, isEmpty); + expect(cubit.state.drawPaths, isEmpty); + expect(cubit.state.currentPageName, isNull); + }, + ); + }); + + group('Undo/Redo', () { + blocTest( + 'undo restores previous state', + build: () => canvasCubit, + act: (cubit) { + cubit.addText('First'); + cubit.addText('Second'); + cubit.undo(); + }, + verify: (cubit) { + expect(cubit.state.textItems.length, 1); + expect(cubit.state.textItems.first.text, 'First'); + }, + ); + + blocTest( + 'redo restores undone state', + build: () => canvasCubit, + act: (cubit) { + cubit.addText('First'); + cubit.addText('Second'); + cubit.undo(); + cubit.redo(); + }, + verify: (cubit) { + expect(cubit.state.textItems.length, 2); + expect(cubit.state.textItems.last.text, 'Second'); + }, + ); + + blocTest( + 'new action clears redo history', + build: () => canvasCubit, + act: (cubit) { + cubit.addText('First'); + cubit.addText('Second'); + cubit.undo(); + cubit.addText('Third'); + }, + verify: (cubit) { + expect(cubit.state.future, isEmpty); + }, + ); + }); + }); +} diff --git a/lib/tests/draw_model_test.dart b/lib/tests/draw_model_test.dart new file mode 100644 index 00000000..de986142 --- /dev/null +++ b/lib/tests/draw_model_test.dart @@ -0,0 +1,436 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:texterra/models/draw_model.dart'; + +void main() { + group('DrawingPoint', () { + test('creates DrawingPoint with offset and paint', () { + final paint = Paint() + ..color = Colors.black + ..strokeWidth = 5.0; + + final point = DrawingPoint( + offset: const Offset(10, 20), + paint: paint, + ); + + expect(point.offset, const Offset(10, 20)); + expect(point.paint.color, Colors.black); + expect(point.paint.strokeWidth, 5.0); + }); + + test('toJson serializes DrawingPoint correctly', () { + final paint = Paint() + ..color = Colors.red + ..strokeWidth = 3.0 + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round; + + final point = DrawingPoint( + offset: const Offset(15.5, 25.5), + paint: paint, + ); + + final json = point.toJson(); + + expect(json['offsetX'], 15.5); + expect(json['offsetY'], 25.5); + expect(json['color'], Colors.red.value); + expect(json['strokeWidth'], 3.0); + expect(json['strokeCap'], StrokeCap.round.index); + expect(json['strokeJoin'], StrokeJoin.round.index); + }); + + test('fromJson deserializes DrawingPoint correctly', () { + final json = { + 'offsetX': 30.5, + 'offsetY': 40.5, + 'color': Colors.blue.value, + 'strokeWidth': 7.0, + 'strokeCap': StrokeCap.square.index, + 'strokeJoin': StrokeJoin.miter.index, + }; + + final point = DrawingPoint.fromJson(json); + + expect(point.offset.dx, 30.5); + expect(point.offset.dy, 40.5); + expect(point.paint.color.value, Colors.blue.value); + expect(point.paint.strokeWidth, 7.0); + expect(point.paint.strokeCap, StrokeCap.square); + expect(point.paint.strokeJoin, StrokeJoin.miter); + }); + + test('serialization round-trip preserves data', () { + final paint = Paint() + ..color = const Color(0xFF123456) + ..strokeWidth = 12.5 + ..strokeCap = StrokeCap.butt + ..strokeJoin = StrokeJoin.bevel; + + final original = DrawingPoint( + offset: const Offset(100.25, 200.75), + paint: paint, + ); + + final json = original.toJson(); + final deserialized = DrawingPoint.fromJson(json); + + expect(deserialized.offset, original.offset); + expect(deserialized.paint.color.value, original.paint.color.value); + expect(deserialized.paint.strokeWidth, original.paint.strokeWidth); + expect(deserialized.paint.strokeCap, original.paint.strokeCap); + expect(deserialized.paint.strokeJoin, original.paint.strokeJoin); + }); + }); + + group('DrawPath', () { + test('creates DrawPath with required parameters', () { + final paint = Paint() + ..color = Colors.black + ..strokeWidth = 5.0; + + final points = [ + DrawingPoint(offset: const Offset(10, 10), paint: paint), + DrawingPoint(offset: const Offset(20, 20), paint: paint), + ]; + + final path = DrawPath( + points: points, + color: Colors.black, + strokeWidth: 5.0, + ); + + expect(path.points.length, 2); + expect(path.color, Colors.black); + expect(path.strokeWidth, 5.0); + expect(path.brushType, BrushType.brush); // Default value + expect(path.isFill, false); // Default value + }); + + test('creates DrawPath with optional parameters', () { + final paint = Paint() + ..color = Colors.red + ..strokeWidth = 3.0; + + final points = [ + DrawingPoint(offset: const Offset(10, 10), paint: paint), + ]; + + final path = DrawPath( + points: points, + color: Colors.red, + strokeWidth: 3.0, + brushType: BrushType.marker, + isFill: true, + ); + + expect(path.brushType, BrushType.marker); + expect(path.isFill, true); + }); + + test('toJson serializes DrawPath correctly', () { + final paint = Paint() + ..color = Colors.green + ..strokeWidth = 4.0; + + final points = [ + DrawingPoint(offset: const Offset(10, 10), paint: paint), + DrawingPoint(offset: const Offset(20, 20), paint: paint), + DrawingPoint(offset: const Offset(30, 30), paint: paint), + ]; + + final path = DrawPath( + points: points, + color: Colors.green, + strokeWidth: 4.0, + brushType: BrushType.marker, + isFill: false, + ); + + final json = path.toJson(); + + expect(json['points'], isA()); + expect(json['points'].length, 3); + expect(json['color'], Colors.green.value); + expect(json['strokeWidth'], 4.0); + expect(json['brushType'], BrushType.marker.index); + expect(json['isFill'], false); + }); + + test('fromJson deserializes DrawPath correctly', () { + final pointsJson = [ + { + 'offsetX': 10.0, + 'offsetY': 10.0, + 'color': Colors.blue.value, + 'strokeWidth': 6.0, + 'strokeCap': StrokeCap.round.index, + 'strokeJoin': StrokeJoin.round.index, + }, + { + 'offsetX': 20.0, + 'offsetY': 20.0, + 'color': Colors.blue.value, + 'strokeWidth': 6.0, + 'strokeCap': StrokeCap.round.index, + 'strokeJoin': StrokeJoin.round.index, + }, + ]; + + final json = { + 'points': pointsJson, + 'color': Colors.blue.value, + 'strokeWidth': 6.0, + 'strokeCap': StrokeCap.round.index, + 'brushType': BrushType.brush.index, + 'isFill': false, + }; + + final path = DrawPath.fromJson(json); + + expect(path.points.length, 2); + expect(path.color.value, Colors.blue.value); + expect(path.strokeWidth, 6.0); + expect(path.brushType, BrushType.brush); + expect(path.isFill, false); + }); + + test('fromJson handles missing optional fields with defaults', () { + final pointsJson = [ + { + 'offsetX': 10.0, + 'offsetY': 10.0, + 'color': Colors.red.value, + 'strokeWidth': 5.0, + 'strokeCap': StrokeCap.round.index, + 'strokeJoin': StrokeJoin.round.index, + }, + ]; + + final json = { + 'points': pointsJson, + 'color': Colors.red.value, + 'strokeWidth': 5.0, + }; + + final path = DrawPath.fromJson(json); + + expect(path.brushType, BrushType.brush); // Default + expect(path.isFill, false); // Default + }); + + test('serialization round-trip preserves all data', () { + final paint = Paint() + ..color = const Color(0xFFABCDEF) + ..strokeWidth = 8.5 + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round; + + final points = [ + DrawingPoint(offset: const Offset(50.5, 60.5), paint: paint), + DrawingPoint(offset: const Offset(70.5, 80.5), paint: paint), + ]; + + final original = DrawPath( + points: points, + color: const Color(0xFFABCDEF), + strokeWidth: 8.5, + brushType: BrushType.marker, + isFill: true, + ); + + final json = original.toJson(); + final deserialized = DrawPath.fromJson(json); + + expect(deserialized.points.length, original.points.length); + expect(deserialized.color.value, original.color.value); + expect(deserialized.strokeWidth, original.strokeWidth); + expect(deserialized.brushType, original.brushType); + expect(deserialized.isFill, original.isFill); + + // Check first point + expect(deserialized.points[0].offset, original.points[0].offset); + expect(deserialized.points[0].paint.color.value, + original.points[0].paint.color.value); + expect(deserialized.points[0].paint.strokeWidth, + original.points[0].paint.strokeWidth); + }); + + test('empty points list is handled correctly', () { + final path = DrawPath( + points: [], + color: Colors.black, + strokeWidth: 5.0, + ); + + expect(path.points, isEmpty); + + final json = path.toJson(); + final deserialized = DrawPath.fromJson(json); + + expect(deserialized.points, isEmpty); + }); + + test('large path with many points serializes correctly', () { + final paint = Paint() + ..color = Colors.purple + ..strokeWidth = 2.0; + + // Create a path with 100 points + final points = List.generate( + 100, + (i) => DrawingPoint( + offset: Offset(i.toDouble(), i.toDouble() * 2), + paint: paint, + ), + ); + + final path = DrawPath( + points: points, + color: Colors.purple, + strokeWidth: 2.0, + ); + + final json = path.toJson(); + final deserialized = DrawPath.fromJson(json); + + expect(deserialized.points.length, 100); + expect(deserialized.points[0].offset, const Offset(0, 0)); + expect(deserialized.points[99].offset, const Offset(99, 198)); + }); + }); + + group('BrushType', () { + test('BrushType enum has correct values', () { + expect(BrushType.values.length, 2); + expect(BrushType.brush.index, 0); + expect(BrushType.marker.index, 1); + }); + + test('BrushType can be serialized and deserialized', () { + final brushIndex = BrushType.brush.index; + final markerIndex = BrushType.marker.index; + + expect(BrushType.values[brushIndex], BrushType.brush); + expect(BrushType.values[markerIndex], BrushType.marker); + }); + }); + + group('DrawPath Edge Cases', () { + test('handles single point path', () { + final paint = Paint() + ..color = Colors.orange + ..strokeWidth = 5.0; + + final path = DrawPath( + points: [DrawingPoint(offset: const Offset(50, 50), paint: paint)], + color: Colors.orange, + strokeWidth: 5.0, + ); + + expect(path.points.length, 1); + + final json = path.toJson(); + final deserialized = DrawPath.fromJson(json); + + expect(deserialized.points.length, 1); + expect(deserialized.points[0].offset, const Offset(50, 50)); + }); + + test('handles fill path correctly', () { + final paint = Paint() + ..color = Colors.cyan + ..strokeWidth = 1.0; + + final path = DrawPath( + points: [DrawingPoint(offset: Offset.zero, paint: paint)], + color: Colors.cyan, + strokeWidth: 1.0, + isFill: true, + ); + + expect(path.isFill, true); + + final json = path.toJson(); + final deserialized = DrawPath.fromJson(json); + + expect(deserialized.isFill, true); + }); + + test('handles different stroke widths in points', () { + final paint1 = Paint() + ..color = Colors.black + ..strokeWidth = 2.0; + + final paint2 = Paint() + ..color = Colors.black + ..strokeWidth = 5.0; + + final paint3 = Paint() + ..color = Colors.black + ..strokeWidth = 10.0; + + final path = DrawPath( + points: [ + DrawingPoint(offset: const Offset(10, 10), paint: paint1), + DrawingPoint(offset: const Offset(20, 20), paint: paint2), + DrawingPoint(offset: const Offset(30, 30), paint: paint3), + ], + color: Colors.black, + strokeWidth: 5.0, + ); + + final json = path.toJson(); + final deserialized = DrawPath.fromJson(json); + + expect(deserialized.points[0].paint.strokeWidth, 2.0); + expect(deserialized.points[1].paint.strokeWidth, 5.0); + expect(deserialized.points[2].paint.strokeWidth, 10.0); + }); + + test('handles negative coordinates', () { + final paint = Paint() + ..color = Colors.red + ..strokeWidth = 5.0; + + final path = DrawPath( + points: [ + DrawingPoint(offset: const Offset(-10, -20), paint: paint), + DrawingPoint(offset: const Offset(-30, -40), paint: paint), + ], + color: Colors.red, + strokeWidth: 5.0, + ); + + final json = path.toJson(); + final deserialized = DrawPath.fromJson(json); + + expect(deserialized.points[0].offset, const Offset(-10, -20)); + expect(deserialized.points[1].offset, const Offset(-30, -40)); + }); + + test('handles very small and very large coordinates', () { + final paint = Paint() + ..color = Colors.blue + ..strokeWidth = 5.0; + + final path = DrawPath( + points: [ + DrawingPoint(offset: const Offset(0.001, 0.001), paint: paint), + DrawingPoint(offset: const Offset(9999.99, 9999.99), paint: paint), + ], + color: Colors.blue, + strokeWidth: 5.0, + ); + + final json = path.toJson(); + final deserialized = DrawPath.fromJson(json); + + expect(deserialized.points[0].offset.dx, closeTo(0.001, 0.0001)); + expect(deserialized.points[0].offset.dy, closeTo(0.001, 0.0001)); + expect(deserialized.points[1].offset.dx, closeTo(9999.99, 0.01)); + expect(deserialized.points[1].offset.dy, closeTo(9999.99, 0.01)); + }); + }); +} diff --git a/lib/tests/text_item_model_test.dart b/lib/tests/text_item_model_test.dart new file mode 100644 index 00000000..5b9c8795 --- /dev/null +++ b/lib/tests/text_item_model_test.dart @@ -0,0 +1,332 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:texterra/models/text_item_model.dart'; + +void main() { + group('TextItem Model', () { + test('creates TextItem with required parameters', () { + final textItem = TextItem( + text: 'Test', + x: 10, + y: 20, + fontSize: 16, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontFamily: 'Roboto', + isUnderlined: false, + color: Colors.black, + ); + + expect(textItem.text, 'Test'); + expect(textItem.x, 10); + expect(textItem.y, 20); + expect(textItem.fontSize, 16); + expect(textItem.fontStyle, FontStyle.normal); + expect(textItem.fontWeight, FontWeight.normal); + expect(textItem.fontFamily, 'Roboto'); + expect(textItem.isUnderlined, false); + expect(textItem.color, Colors.black); + }); + + test('creates TextItem with default optional parameters', () { + final textItem = TextItem( + text: 'Test', + x: 10, + y: 20, + fontSize: 16, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontFamily: 'Roboto', + isUnderlined: false, + color: Colors.black, + ); + + expect(textItem.isHighlighted, false); + expect(textItem.highlightColor, isNull); + expect(textItem.textAlign, TextAlign.left); + expect(textItem.hasShadow, false); + expect(textItem.shadowColor, Colors.black); + expect(textItem.shadowBlurRadius, 4.0); + expect(textItem.shadowOffset, const Offset(2.0, 2.0)); + }); + + test('toHTML generates correct HTML with basic styling', () { + final textItem = TextItem( + text: 'Test', + x: 10, + y: 20, + fontSize: 16, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontFamily: 'Arial', + isUnderlined: false, + color: Colors.black, + ); + + final html = textItem.toHTML(); + + expect(html, contains('font-size: 16.0px')); + expect(html, contains('font-family: Arial')); + expect(html, contains('font-style: normal')); + }); + + test('toHTML includes underline decoration', () { + final textItem = TextItem( + text: 'Test', + x: 10, + y: 20, + fontSize: 16, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontFamily: 'Arial', + isUnderlined: true, + color: Colors.black, + ); + + final html = textItem.toHTML(); + + expect(html, contains('text-decoration: underline')); + }); + + test('toHTML includes highlight background color', () { + final textItem = TextItem( + text: 'Test', + x: 10, + y: 20, + fontSize: 16, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontFamily: 'Arial', + isUnderlined: false, + color: Colors.black, + isHighlighted: true, + highlightColor: Colors.yellow, + ); + + final html = textItem.toHTML(); + + expect(html, contains('background-color:')); + }); + + test('toHTML includes text shadow', () { + final textItem = TextItem( + text: 'Test', + x: 10, + y: 20, + fontSize: 16, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontFamily: 'Arial', + isUnderlined: false, + color: Colors.black, + hasShadow: true, + shadowColor: Colors.grey, + shadowBlurRadius: 4.0, + shadowOffset: const Offset(2.0, 2.0), + ); + + final html = textItem.toHTML(); + + expect(html, contains('text-shadow:')); + }); + + test('copyWith creates new instance with updated values', () { + final original = TextItem( + text: 'Original', + x: 10, + y: 20, + fontSize: 16, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontFamily: 'Roboto', + isUnderlined: false, + color: Colors.black, + ); + + final updated = original.copyWith( + text: 'Updated', + fontSize: 24, + color: const Color(0xFFF44336), + ); + + expect(updated.text, 'Updated'); + expect(updated.fontSize, 24); + expect(updated.color, const Color(0xFFF44336)); + expect(updated.x, 10); // Unchanged + expect(updated.y, 20); // Unchanged + }); + + test('copyWith preserves original values when not specified', () { + final original = TextItem( + text: 'Test', + x: 10, + y: 20, + fontSize: 16, + fontStyle: FontStyle.italic, + fontWeight: FontWeight.bold, + fontFamily: 'Arial', + isUnderlined: true, + color: const Color(0xFF2196F3), + isHighlighted: true, + highlightColor: const Color(0xFFFFEB3B), + ); + + final updated = original.copyWith(text: 'Updated'); + + expect(updated.text, 'Updated'); + expect(updated.fontStyle, FontStyle.italic); + expect(updated.fontWeight, FontWeight.bold); + expect(updated.fontFamily, 'Arial'); + expect(updated.isUnderlined, true); + expect(updated.isHighlighted, true); + expect(updated.highlightColor, const Color(0xFFFFEB3B)); + }); + + test('toJson serializes TextItem correctly', () { + final textItem = TextItem( + text: 'Test', + x: 10, + y: 20, + fontSize: 16, + fontStyle: FontStyle.italic, + fontWeight: FontWeight.bold, + fontFamily: 'Arial', + isUnderlined: true, + color: const Color(0xFFF44336), + isHighlighted: true, + highlightColor: const Color(0xFFFFEB3B), + textAlign: TextAlign.center, + hasShadow: true, + shadowColor: Colors.black, + shadowBlurRadius: 5.0, + shadowOffset: const Offset(3.0, 3.0), + ); + + final json = textItem.toJson(); + + expect(json['text'], 'Test'); + expect(json['x'], 10); + expect(json['y'], 20); + expect(json['fontSize'], 16); + expect(json['fontStyle'], FontStyle.italic.index); + expect(json['fontWeight'], FontWeight.bold.index); + expect(json['fontFamily'], 'Arial'); + expect(json['isUnderlined'], true); + expect(json['color'], 0xFFF44336); + expect(json['isHighlighted'], true); + expect(json['highlightColor'], 0xFFFFEB3B); + expect(json['textAlign'], TextAlign.center.index); + expect(json['hasShadow'], true); + expect(json['shadowColor'], Colors.black.value); + expect(json['shadowBlurRadius'], 5.0); + expect(json['shadowOffsetDx'], 3.0); + expect(json['shadowOffsetDy'], 3.0); + }); + + test('fromJson deserializes TextItem correctly', () { + final json = { + 'text': 'Test', + 'x': 10.0, + 'y': 20.0, + 'fontSize': 16.0, + 'fontStyle': FontStyle.italic.index, + 'fontWeight': FontWeight.bold.index, + 'fontFamily': 'Arial', + 'isUnderlined': true, + 'color': 0xFFF44336, + 'isHighlighted': true, + 'highlightColor': 0xFFFFEB3B, + 'textAlign': TextAlign.center.index, + 'hasShadow': true, + 'shadowColor': 0xFF000000, + 'shadowBlurRadius': 5.0, + 'shadowOffsetDx': 3.0, + 'shadowOffsetDy': 3.0, + }; + + final textItem = TextItem.fromJson(json); + + expect(textItem.text, 'Test'); + expect(textItem.x, 10); + expect(textItem.y, 20); + expect(textItem.fontSize, 16); + expect(textItem.fontStyle, FontStyle.italic); + expect(textItem.fontWeight, FontWeight.bold); + expect(textItem.fontFamily, 'Arial'); + expect(textItem.isUnderlined, true); + expect(textItem.color, const Color(0xFFF44336)); + expect(textItem.isHighlighted, true); + expect(textItem.highlightColor, const Color(0xFFFFEB3B)); + expect(textItem.textAlign, TextAlign.center); + expect(textItem.hasShadow, true); + expect(textItem.shadowColor, const Color(0xFF000000)); + expect(textItem.shadowBlurRadius, 5.0); + expect(textItem.shadowOffset, const Offset(3.0, 3.0)); + }); + + test('fromJson handles missing optional fields with defaults', () { + final json = { + 'text': 'Test', + 'x': 10.0, + 'y': 20.0, + 'fontSize': 16.0, + 'fontStyle': FontStyle.normal.index, + 'fontWeight': FontWeight.normal.index, + 'fontFamily': 'Roboto', + 'color': Colors.black.value, + }; + + final textItem = TextItem.fromJson(json); + + expect(textItem.isUnderlined, false); + expect(textItem.textAlign, TextAlign.left); + expect(textItem.isHighlighted, false); + expect(textItem.highlightColor, isNull); + expect(textItem.hasShadow, false); + expect(textItem.shadowColor, Colors.black); + expect(textItem.shadowBlurRadius, 4.0); + expect(textItem.shadowOffset, const Offset(2.0, 2.0)); + }); + + test('serialization round-trip preserves all data', () { + final original = TextItem( + text: 'Test Round Trip', + x: 15.5, + y: 25.5, + fontSize: 18, + fontStyle: FontStyle.italic, + fontWeight: FontWeight.w600, + fontFamily: 'Helvetica', + isUnderlined: true, + color: const Color(0xFF123456), + isHighlighted: true, + highlightColor: const Color(0xFFABCDEF), + textAlign: TextAlign.right, + hasShadow: true, + shadowColor: const Color(0xFF111111), + shadowBlurRadius: 7.5, + shadowOffset: const Offset(4.5, 5.5), + ); + + final json = original.toJson(); + final deserialized = TextItem.fromJson(json); + + expect(deserialized.text, original.text); + expect(deserialized.x, original.x); + expect(deserialized.y, original.y); + expect(deserialized.fontSize, original.fontSize); + expect(deserialized.fontStyle, original.fontStyle); + expect(deserialized.fontWeight, original.fontWeight); + expect(deserialized.fontFamily, original.fontFamily); + expect(deserialized.isUnderlined, original.isUnderlined); + expect(deserialized.color, original.color); + expect(deserialized.isHighlighted, original.isHighlighted); + expect(deserialized.highlightColor, original.highlightColor); + expect(deserialized.textAlign, original.textAlign); + expect(deserialized.hasShadow, original.hasShadow); + expect(deserialized.shadowColor, original.shadowColor); + expect(deserialized.shadowBlurRadius, original.shadowBlurRadius); + expect(deserialized.shadowOffset, original.shadowOffset); + }); + }); +} diff --git a/lib/tests/widgets_test.dart b/lib/tests/widgets_test.dart new file mode 100644 index 00000000..6f609187 --- /dev/null +++ b/lib/tests/widgets_test.dart @@ -0,0 +1,558 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:texterra/cubit/canvas_cubit.dart'; +import 'package:texterra/cubit/canvas_state.dart'; +import 'package:texterra/ui/widgets/editable_text_widget.dart'; +import 'package:texterra/ui/widgets/background_color_tray.dart'; +import 'package:texterra/ui/widgets/background_options_sheet.dart'; +import 'package:texterra/models/text_item_model.dart'; +import 'package:texterra/constants/color_constants.dart'; + +void main() { + group('EditableTextWidget', () { + late CanvasCubit canvasCubit; + + setUp(() { + canvasCubit = CanvasCubit(); + }); + + tearDown(() { + canvasCubit.close(); + }); + + testWidgets('displays text correctly', (tester) async { + final textItem = TextItem( + text: 'Test Text', + x: 10, + y: 20, + fontSize: 16, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontFamily: 'Roboto', + isUnderlined: false, + color: Colors.white, + ); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: Scaffold( + body: EditableTextWidget( + index: 0, + textItem: textItem, + isSelected: false, + ), + ), + ), + ), + ); + + expect(find.text('Test Text'), findsOneWidget); + }); + + testWidgets('shows highlight when selected', (tester) async { + final textItem = TextItem( + text: 'Selected Text', + x: 10, + y: 20, + fontSize: 16, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontFamily: 'Roboto', + isUnderlined: false, + color: Colors.white, + ); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: Scaffold( + body: EditableTextWidget( + index: 0, + textItem: textItem, + isSelected: true, + ), + ), + ), + ), + ); + + final textWidget = tester.widget(find.text('Selected Text')); + expect(textWidget.style?.backgroundColor, isNotNull); + }); + + testWidgets('applies font styling correctly', (tester) async { + final textItem = TextItem( + text: 'Styled Text', + x: 10, + y: 20, + fontSize: 24, + fontStyle: FontStyle.italic, + fontWeight: FontWeight.bold, + fontFamily: 'Arial', + isUnderlined: true, + color: Colors.red, + ); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: Scaffold( + body: EditableTextWidget( + index: 0, + textItem: textItem, + isSelected: false, + ), + ), + ), + ), + ); + + final textWidget = tester.widget(find.text('Styled Text')); + expect(textWidget.style?.fontSize, 24); + expect(textWidget.style?.fontStyle, FontStyle.italic); + expect(textWidget.style?.fontWeight, FontWeight.bold); + expect(textWidget.style?.decoration, TextDecoration.underline); + expect(textWidget.style?.color, Colors.red); + }); + + testWidgets('shows highlight color when highlighted', (tester) async { + final textItem = TextItem( + text: 'Highlighted Text', + x: 10, + y: 20, + fontSize: 16, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontFamily: 'Roboto', + isUnderlined: false, + color: Colors.black, + isHighlighted: true, + highlightColor: Colors.yellow, + ); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: Scaffold( + body: EditableTextWidget( + index: 0, + textItem: textItem, + isSelected: false, + ), + ), + ), + ), + ); + + final textWidget = tester.widget(find.text('Highlighted Text')); + expect(textWidget.style?.backgroundColor, Colors.yellow); + }); + + testWidgets('applies text shadow when enabled', (tester) async { + final textItem = TextItem( + text: 'Shadow Text', + x: 10, + y: 20, + fontSize: 16, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontFamily: 'Roboto', + isUnderlined: false, + color: Colors.white, + hasShadow: true, + shadowColor: Colors.black, + shadowBlurRadius: 4.0, + shadowOffset: const Offset(2.0, 2.0), + ); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: Scaffold( + body: EditableTextWidget( + index: 0, + textItem: textItem, + isSelected: false, + ), + ), + ), + ), + ); + + final textWidget = tester.widget(find.text('Shadow Text')); + expect(textWidget.style?.shadows, isNotNull); + expect(textWidget.style?.shadows?.length, 1); + }); + + testWidgets('applies text alignment', (tester) async { + final textItem = TextItem( + text: 'Centered Text', + x: 10, + y: 20, + fontSize: 16, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontFamily: 'Roboto', + isUnderlined: false, + color: Colors.white, + textAlign: TextAlign.center, + ); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: Scaffold( + body: EditableTextWidget( + index: 0, + textItem: textItem, + isSelected: false, + ), + ), + ), + ), + ); + + final textWidget = tester.widget(find.text('Centered Text')); + expect(textWidget.textAlign, TextAlign.center); + }); + + testWidgets('opens edit dialog on tap', (tester) async { + final textItem = TextItem( + text: 'Tap Me', + x: 10, + y: 20, + fontSize: 16, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontFamily: 'Roboto', + isUnderlined: false, + color: Colors.white, + ); + + canvasCubit.emit(CanvasState.initial().copyWith( + textItems: [textItem], + )); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: Scaffold( + body: EditableTextWidget( + index: 0, + textItem: textItem, + isSelected: false, + ), + ), + ), + ), + ); + + await tester.tap(find.text('Tap Me')); + await tester.pumpAndSettle(); + + expect(find.text('Edit Text'), findsOneWidget); + expect(find.byType(TextFormField), findsOneWidget); + }); + }); + + group('BackgroundColorTray', () { + late CanvasCubit canvasCubit; + + setUp(() { + canvasCubit = CanvasCubit(); + }); + + tearDown(() { + canvasCubit.close(); + }); + + testWidgets('displays all background colors', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: const Scaffold( + body: BackgroundColorTray(), + ), + ), + ), + ); + + expect(find.text('Background Color'), findsOneWidget); + + // Should find color selection containers + final colorContainers = find.byType(GestureDetector); + expect(colorContainers, findsWidgets); + }); + + testWidgets('shows selected color with checkmark', (tester) async { + canvasCubit.emit(CanvasState.initial().copyWith( + backgroundColor: ColorConstants.backgroundDeepPurple, + )); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: const Scaffold( + body: BackgroundColorTray(), + ), + ), + ), + ); + + await tester.pump(); + + // Should show check icon for selected color + expect(find.byIcon(Icons.check), findsOneWidget); + }); + + testWidgets('changes background color on tap', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: const Scaffold( + body: BackgroundColorTray(), + ), + ), + ), + ); + + final initialColor = canvasCubit.state.backgroundColor; + + // Tap on a color container + await tester.tap(find.byType(GestureDetector).first); + await tester.pumpAndSettle(); + + // Background color should have changed + expect(canvasCubit.state.backgroundColor, isNot(equals(initialColor))); + }); + }); + + group('BackgroundOptionsSheet', () { + late CanvasCubit canvasCubit; + + setUp(() { + canvasCubit = CanvasCubit(); + }); + + tearDown(() { + canvasCubit.close(); + }); + + testWidgets('displays all background options', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: const Scaffold( + body: BackgroundOptionsSheet(), + ), + ), + ), + ); + + expect(find.text('Background Options'), findsOneWidget); + expect(find.text('Upload Image'), findsOneWidget); + expect(find.text('Take Photo'), findsOneWidget); + expect(find.text('Solid Color'), findsOneWidget); + expect(find.text('Remove Image'), findsOneWidget); + }); + + testWidgets('upload image option is always enabled', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: const Scaffold( + body: BackgroundOptionsSheet(), + ), + ), + ), + ); + + final uploadTile = find.ancestor( + of: find.text('Upload Image'), + matching: find.byType(InkWell), + ); + + expect(uploadTile, findsOneWidget); + }); + + testWidgets('remove image is disabled when no background image', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: const Scaffold( + body: BackgroundOptionsSheet(), + ), + ), + ), + ); + + await tester.pump(); + + // Remove Image should be visible but might be disabled + expect(find.text('Remove Image'), findsOneWidget); + }); + + testWidgets('remove image is enabled when background image exists', + (tester) async { + canvasCubit.emit(CanvasState.initial().copyWith( + backgroundImagePath: '/fake/path/image.png', + )); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: const Scaffold( + body: BackgroundOptionsSheet(), + ), + ), + ), + ); + + await tester.pump(); + + expect(find.text('Remove Image'), findsOneWidget); + }); + + testWidgets('displays correct icons for each option', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: const Scaffold( + body: BackgroundOptionsSheet(), + ), + ), + ), + ); + + expect(find.byIcon(Icons.photo_library), findsOneWidget); + expect(find.byIcon(Icons.camera_alt), findsOneWidget); + expect(find.byIcon(Icons.color_lens), findsOneWidget); + expect(find.byIcon(Icons.clear), findsOneWidget); + }); + }); + + group('EditTextDialog', () { + testWidgets('displays with initial text', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: EditTextDialog(initialText: 'Initial Text'), + ), + ), + ); + + expect(find.text('Edit Text'), findsOneWidget); + expect(find.text('Initial Text'), findsOneWidget); + expect(find.text('Remove'), findsOneWidget); + expect(find.text('Save'), findsOneWidget); + }); + + testWidgets('validates empty text', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: EditTextDialog(initialText: 'Test'), + ), + ), + ); + + // Clear the text field + await tester.enterText(find.byType(TextFormField), ''); + + // Try to save + await tester.tap(find.text('Save')); + await tester.pumpAndSettle(); + + // Should show validation error + expect(find.text('Text cannot be empty'), findsOneWidget); + }); + + testWidgets('saves text on save button tap', (tester) async { + String? savedText; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + return ElevatedButton( + onPressed: () async { + final result = await showDialog( + context: context, + builder: (context) => const EditTextDialog( + initialText: 'Initial', + ), + ); + savedText = result; + }, + child: const Text('Open Dialog'), + ); + }, + ), + ), + ), + ); + + await tester.tap(find.text('Open Dialog')); + await tester.pumpAndSettle(); + + await tester.enterText(find.byType(TextFormField), 'Updated Text'); + await tester.tap(find.text('Save')); + await tester.pumpAndSettle(); + + expect(savedText, 'Updated Text'); + }); + + testWidgets('returns delete signal on remove button tap', (tester) async { + String? result; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + return ElevatedButton( + onPressed: () async { + result = await showDialog( + context: context, + builder: (context) => const EditTextDialog( + initialText: 'Test', + ), + ); + }, + child: const Text('Open Dialog'), + ); + }, + ), + ), + ), + ); + + await tester.tap(find.text('Open Dialog')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Remove')); + await tester.pumpAndSettle(); + + expect(result, '_delete_'); + }); + }); +} diff --git a/pubspec.yaml b/pubspec.yaml index da09048a..ccfbc8fd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,7 @@ dependencies: image_picker: ^1.2.0 path_provider: ^2.1.5 path: ^1.9.1 + bloc_test: ^10.0.0 dev_dependencies: flutter_test: diff --git a/test/widget_test.dart b/test/widget_test.dart index 4c6633eb..860ec873 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,29 +1,49 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:texterra/main.dart'; +import 'package:texterra/ui/screens/splash_screen.dart'; +import 'package:texterra/cubit/canvas_cubit.dart'; void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { + testWidgets('App initializes and shows splash screen', + (WidgetTester tester) async { // Build our app and trigger a frame. await tester.pumpWidget(const MyApp()); - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); + // Verify that the app builds successfully + expect(find.byType(MaterialApp), findsOneWidget); + + // Verify that SplashScreen is shown initially + expect(find.byType(SplashScreen), findsOneWidget); + + // Wait for all timers and animations to complete + await tester.pumpAndSettle(); + }); + + testWidgets('App has correct title', (WidgetTester tester) async { + // Build our app + await tester.pumpWidget(const MyApp()); + + // Get the MaterialApp widget + final MaterialApp app = tester.widget(find.byType(MaterialApp)); + + // Verify the title + expect(app.title, 'Text Editor'); + + // Wait for all timers and animations to complete + await tester.pumpAndSettle(); + }); + + testWidgets('App has BlocProvider for CanvasCubit', + (WidgetTester tester) async { + // Build our app + await tester.pumpWidget(const MyApp()); - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); + // Verify that BlocProvider is present in the widget tree + expect(find.byType(BlocProvider), findsOneWidget); - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); + // Wait for all timers and animations to complete + await tester.pumpAndSettle(); }); } From ab9a0042450d6e4af3c3295d1d2eba850da1dd50 Mon Sep 17 00:00:00 2001 From: preetidas60 Date: Mon, 20 Oct 2025 13:00:25 +0530 Subject: [PATCH 2/4] Implemented Comprehensive Testing Suite with Main Testing --- lib/tests/widgets_test.dart | 558 ---------------------------------- test/widget_test.dart | 581 ++++++++++++++++++++++++++++++++++-- 2 files changed, 552 insertions(+), 587 deletions(-) delete mode 100644 lib/tests/widgets_test.dart diff --git a/lib/tests/widgets_test.dart b/lib/tests/widgets_test.dart deleted file mode 100644 index 6f609187..00000000 --- a/lib/tests/widgets_test.dart +++ /dev/null @@ -1,558 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:texterra/cubit/canvas_cubit.dart'; -import 'package:texterra/cubit/canvas_state.dart'; -import 'package:texterra/ui/widgets/editable_text_widget.dart'; -import 'package:texterra/ui/widgets/background_color_tray.dart'; -import 'package:texterra/ui/widgets/background_options_sheet.dart'; -import 'package:texterra/models/text_item_model.dart'; -import 'package:texterra/constants/color_constants.dart'; - -void main() { - group('EditableTextWidget', () { - late CanvasCubit canvasCubit; - - setUp(() { - canvasCubit = CanvasCubit(); - }); - - tearDown(() { - canvasCubit.close(); - }); - - testWidgets('displays text correctly', (tester) async { - final textItem = TextItem( - text: 'Test Text', - x: 10, - y: 20, - fontSize: 16, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontFamily: 'Roboto', - isUnderlined: false, - color: Colors.white, - ); - - await tester.pumpWidget( - MaterialApp( - home: BlocProvider.value( - value: canvasCubit, - child: Scaffold( - body: EditableTextWidget( - index: 0, - textItem: textItem, - isSelected: false, - ), - ), - ), - ), - ); - - expect(find.text('Test Text'), findsOneWidget); - }); - - testWidgets('shows highlight when selected', (tester) async { - final textItem = TextItem( - text: 'Selected Text', - x: 10, - y: 20, - fontSize: 16, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontFamily: 'Roboto', - isUnderlined: false, - color: Colors.white, - ); - - await tester.pumpWidget( - MaterialApp( - home: BlocProvider.value( - value: canvasCubit, - child: Scaffold( - body: EditableTextWidget( - index: 0, - textItem: textItem, - isSelected: true, - ), - ), - ), - ), - ); - - final textWidget = tester.widget(find.text('Selected Text')); - expect(textWidget.style?.backgroundColor, isNotNull); - }); - - testWidgets('applies font styling correctly', (tester) async { - final textItem = TextItem( - text: 'Styled Text', - x: 10, - y: 20, - fontSize: 24, - fontStyle: FontStyle.italic, - fontWeight: FontWeight.bold, - fontFamily: 'Arial', - isUnderlined: true, - color: Colors.red, - ); - - await tester.pumpWidget( - MaterialApp( - home: BlocProvider.value( - value: canvasCubit, - child: Scaffold( - body: EditableTextWidget( - index: 0, - textItem: textItem, - isSelected: false, - ), - ), - ), - ), - ); - - final textWidget = tester.widget(find.text('Styled Text')); - expect(textWidget.style?.fontSize, 24); - expect(textWidget.style?.fontStyle, FontStyle.italic); - expect(textWidget.style?.fontWeight, FontWeight.bold); - expect(textWidget.style?.decoration, TextDecoration.underline); - expect(textWidget.style?.color, Colors.red); - }); - - testWidgets('shows highlight color when highlighted', (tester) async { - final textItem = TextItem( - text: 'Highlighted Text', - x: 10, - y: 20, - fontSize: 16, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontFamily: 'Roboto', - isUnderlined: false, - color: Colors.black, - isHighlighted: true, - highlightColor: Colors.yellow, - ); - - await tester.pumpWidget( - MaterialApp( - home: BlocProvider.value( - value: canvasCubit, - child: Scaffold( - body: EditableTextWidget( - index: 0, - textItem: textItem, - isSelected: false, - ), - ), - ), - ), - ); - - final textWidget = tester.widget(find.text('Highlighted Text')); - expect(textWidget.style?.backgroundColor, Colors.yellow); - }); - - testWidgets('applies text shadow when enabled', (tester) async { - final textItem = TextItem( - text: 'Shadow Text', - x: 10, - y: 20, - fontSize: 16, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontFamily: 'Roboto', - isUnderlined: false, - color: Colors.white, - hasShadow: true, - shadowColor: Colors.black, - shadowBlurRadius: 4.0, - shadowOffset: const Offset(2.0, 2.0), - ); - - await tester.pumpWidget( - MaterialApp( - home: BlocProvider.value( - value: canvasCubit, - child: Scaffold( - body: EditableTextWidget( - index: 0, - textItem: textItem, - isSelected: false, - ), - ), - ), - ), - ); - - final textWidget = tester.widget(find.text('Shadow Text')); - expect(textWidget.style?.shadows, isNotNull); - expect(textWidget.style?.shadows?.length, 1); - }); - - testWidgets('applies text alignment', (tester) async { - final textItem = TextItem( - text: 'Centered Text', - x: 10, - y: 20, - fontSize: 16, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontFamily: 'Roboto', - isUnderlined: false, - color: Colors.white, - textAlign: TextAlign.center, - ); - - await tester.pumpWidget( - MaterialApp( - home: BlocProvider.value( - value: canvasCubit, - child: Scaffold( - body: EditableTextWidget( - index: 0, - textItem: textItem, - isSelected: false, - ), - ), - ), - ), - ); - - final textWidget = tester.widget(find.text('Centered Text')); - expect(textWidget.textAlign, TextAlign.center); - }); - - testWidgets('opens edit dialog on tap', (tester) async { - final textItem = TextItem( - text: 'Tap Me', - x: 10, - y: 20, - fontSize: 16, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontFamily: 'Roboto', - isUnderlined: false, - color: Colors.white, - ); - - canvasCubit.emit(CanvasState.initial().copyWith( - textItems: [textItem], - )); - - await tester.pumpWidget( - MaterialApp( - home: BlocProvider.value( - value: canvasCubit, - child: Scaffold( - body: EditableTextWidget( - index: 0, - textItem: textItem, - isSelected: false, - ), - ), - ), - ), - ); - - await tester.tap(find.text('Tap Me')); - await tester.pumpAndSettle(); - - expect(find.text('Edit Text'), findsOneWidget); - expect(find.byType(TextFormField), findsOneWidget); - }); - }); - - group('BackgroundColorTray', () { - late CanvasCubit canvasCubit; - - setUp(() { - canvasCubit = CanvasCubit(); - }); - - tearDown(() { - canvasCubit.close(); - }); - - testWidgets('displays all background colors', (tester) async { - await tester.pumpWidget( - MaterialApp( - home: BlocProvider.value( - value: canvasCubit, - child: const Scaffold( - body: BackgroundColorTray(), - ), - ), - ), - ); - - expect(find.text('Background Color'), findsOneWidget); - - // Should find color selection containers - final colorContainers = find.byType(GestureDetector); - expect(colorContainers, findsWidgets); - }); - - testWidgets('shows selected color with checkmark', (tester) async { - canvasCubit.emit(CanvasState.initial().copyWith( - backgroundColor: ColorConstants.backgroundDeepPurple, - )); - - await tester.pumpWidget( - MaterialApp( - home: BlocProvider.value( - value: canvasCubit, - child: const Scaffold( - body: BackgroundColorTray(), - ), - ), - ), - ); - - await tester.pump(); - - // Should show check icon for selected color - expect(find.byIcon(Icons.check), findsOneWidget); - }); - - testWidgets('changes background color on tap', (tester) async { - await tester.pumpWidget( - MaterialApp( - home: BlocProvider.value( - value: canvasCubit, - child: const Scaffold( - body: BackgroundColorTray(), - ), - ), - ), - ); - - final initialColor = canvasCubit.state.backgroundColor; - - // Tap on a color container - await tester.tap(find.byType(GestureDetector).first); - await tester.pumpAndSettle(); - - // Background color should have changed - expect(canvasCubit.state.backgroundColor, isNot(equals(initialColor))); - }); - }); - - group('BackgroundOptionsSheet', () { - late CanvasCubit canvasCubit; - - setUp(() { - canvasCubit = CanvasCubit(); - }); - - tearDown(() { - canvasCubit.close(); - }); - - testWidgets('displays all background options', (tester) async { - await tester.pumpWidget( - MaterialApp( - home: BlocProvider.value( - value: canvasCubit, - child: const Scaffold( - body: BackgroundOptionsSheet(), - ), - ), - ), - ); - - expect(find.text('Background Options'), findsOneWidget); - expect(find.text('Upload Image'), findsOneWidget); - expect(find.text('Take Photo'), findsOneWidget); - expect(find.text('Solid Color'), findsOneWidget); - expect(find.text('Remove Image'), findsOneWidget); - }); - - testWidgets('upload image option is always enabled', (tester) async { - await tester.pumpWidget( - MaterialApp( - home: BlocProvider.value( - value: canvasCubit, - child: const Scaffold( - body: BackgroundOptionsSheet(), - ), - ), - ), - ); - - final uploadTile = find.ancestor( - of: find.text('Upload Image'), - matching: find.byType(InkWell), - ); - - expect(uploadTile, findsOneWidget); - }); - - testWidgets('remove image is disabled when no background image', - (tester) async { - await tester.pumpWidget( - MaterialApp( - home: BlocProvider.value( - value: canvasCubit, - child: const Scaffold( - body: BackgroundOptionsSheet(), - ), - ), - ), - ); - - await tester.pump(); - - // Remove Image should be visible but might be disabled - expect(find.text('Remove Image'), findsOneWidget); - }); - - testWidgets('remove image is enabled when background image exists', - (tester) async { - canvasCubit.emit(CanvasState.initial().copyWith( - backgroundImagePath: '/fake/path/image.png', - )); - - await tester.pumpWidget( - MaterialApp( - home: BlocProvider.value( - value: canvasCubit, - child: const Scaffold( - body: BackgroundOptionsSheet(), - ), - ), - ), - ); - - await tester.pump(); - - expect(find.text('Remove Image'), findsOneWidget); - }); - - testWidgets('displays correct icons for each option', (tester) async { - await tester.pumpWidget( - MaterialApp( - home: BlocProvider.value( - value: canvasCubit, - child: const Scaffold( - body: BackgroundOptionsSheet(), - ), - ), - ), - ); - - expect(find.byIcon(Icons.photo_library), findsOneWidget); - expect(find.byIcon(Icons.camera_alt), findsOneWidget); - expect(find.byIcon(Icons.color_lens), findsOneWidget); - expect(find.byIcon(Icons.clear), findsOneWidget); - }); - }); - - group('EditTextDialog', () { - testWidgets('displays with initial text', (tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Scaffold( - body: EditTextDialog(initialText: 'Initial Text'), - ), - ), - ); - - expect(find.text('Edit Text'), findsOneWidget); - expect(find.text('Initial Text'), findsOneWidget); - expect(find.text('Remove'), findsOneWidget); - expect(find.text('Save'), findsOneWidget); - }); - - testWidgets('validates empty text', (tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Scaffold( - body: EditTextDialog(initialText: 'Test'), - ), - ), - ); - - // Clear the text field - await tester.enterText(find.byType(TextFormField), ''); - - // Try to save - await tester.tap(find.text('Save')); - await tester.pumpAndSettle(); - - // Should show validation error - expect(find.text('Text cannot be empty'), findsOneWidget); - }); - - testWidgets('saves text on save button tap', (tester) async { - String? savedText; - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Builder( - builder: (context) { - return ElevatedButton( - onPressed: () async { - final result = await showDialog( - context: context, - builder: (context) => const EditTextDialog( - initialText: 'Initial', - ), - ); - savedText = result; - }, - child: const Text('Open Dialog'), - ); - }, - ), - ), - ), - ); - - await tester.tap(find.text('Open Dialog')); - await tester.pumpAndSettle(); - - await tester.enterText(find.byType(TextFormField), 'Updated Text'); - await tester.tap(find.text('Save')); - await tester.pumpAndSettle(); - - expect(savedText, 'Updated Text'); - }); - - testWidgets('returns delete signal on remove button tap', (tester) async { - String? result; - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Builder( - builder: (context) { - return ElevatedButton( - onPressed: () async { - result = await showDialog( - context: context, - builder: (context) => const EditTextDialog( - initialText: 'Test', - ), - ); - }, - child: const Text('Open Dialog'), - ); - }, - ), - ), - ), - ); - - await tester.tap(find.text('Open Dialog')); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Remove')); - await tester.pumpAndSettle(); - - expect(result, '_delete_'); - }); - }); -} diff --git a/test/widget_test.dart b/test/widget_test.dart index 860ec873..61b8b81d 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,49 +1,572 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:texterra/main.dart'; -import 'package:texterra/ui/screens/splash_screen.dart'; import 'package:texterra/cubit/canvas_cubit.dart'; +import 'package:texterra/cubit/canvas_state.dart'; +import 'package:texterra/ui/widgets/editable_text_widget.dart'; +import 'package:texterra/ui/widgets/background_color_tray.dart'; +import 'package:texterra/ui/widgets/background_options_sheet.dart'; +import 'package:texterra/models/text_item_model.dart'; +import 'package:texterra/constants/color_constants.dart'; void main() { - testWidgets('App initializes and shows splash screen', - (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); + group('EditableTextWidget', () { + late CanvasCubit canvasCubit; - // Verify that the app builds successfully - expect(find.byType(MaterialApp), findsOneWidget); + setUp(() { + canvasCubit = CanvasCubit(); + }); - // Verify that SplashScreen is shown initially - expect(find.byType(SplashScreen), findsOneWidget); + tearDown(() { + canvasCubit.close(); + }); - // Wait for all timers and animations to complete - await tester.pumpAndSettle(); + testWidgets('displays text correctly', (tester) async { + final textItem = TextItem( + text: 'Test Text', + x: 10, + y: 20, + fontSize: 16, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontFamily: 'Roboto', + isUnderlined: false, + color: Colors.white, + ); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: Scaffold( + body: EditableTextWidget( + index: 0, + textItem: textItem, + isSelected: false, + ), + ), + ), + ), + ); + + expect(find.text('Test Text'), findsOneWidget); + }); + + testWidgets('shows highlight when selected', (tester) async { + final textItem = TextItem( + text: 'Selected Text', + x: 10, + y: 20, + fontSize: 16, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontFamily: 'Roboto', + isUnderlined: false, + color: Colors.white, + ); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: Scaffold( + body: EditableTextWidget( + index: 0, + textItem: textItem, + isSelected: true, + ), + ), + ), + ), + ); + + final textWidget = tester.widget(find.text('Selected Text')); + expect(textWidget.style?.backgroundColor, isNotNull); + }); + + testWidgets('applies font styling correctly', (tester) async { + final textItem = TextItem( + text: 'Styled Text', + x: 10, + y: 20, + fontSize: 24, + fontStyle: FontStyle.italic, + fontWeight: FontWeight.bold, + fontFamily: 'Roboto', // Changed from 'Arial' to 'Roboto' + isUnderlined: true, + color: Colors.red, + ); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: Scaffold( + body: EditableTextWidget( + index: 0, + textItem: textItem, + isSelected: false, + ), + ), + ), + ), + ); + + final textWidget = tester.widget(find.text('Styled Text')); + expect(textWidget.style?.fontSize, 24); + expect(textWidget.style?.fontStyle, FontStyle.italic); + expect(textWidget.style?.fontWeight, FontWeight.bold); + expect(textWidget.style?.decoration, TextDecoration.underline); + expect(textWidget.style?.color, Colors.red); + }); + + testWidgets('shows highlight color when highlighted', (tester) async { + final textItem = TextItem( + text: 'Highlighted Text', + x: 10, + y: 20, + fontSize: 16, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontFamily: 'Roboto', + isUnderlined: false, + color: Colors.black, + isHighlighted: true, + highlightColor: Colors.yellow, + ); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: Scaffold( + body: EditableTextWidget( + index: 0, + textItem: textItem, + isSelected: false, + ), + ), + ), + ), + ); + + final textWidget = tester.widget(find.text('Highlighted Text')); + expect(textWidget.style?.backgroundColor, Colors.yellow); + }); + + testWidgets('applies text shadow when enabled', (tester) async { + final textItem = TextItem( + text: 'Shadow Text', + x: 10, + y: 20, + fontSize: 16, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontFamily: 'Roboto', + isUnderlined: false, + color: Colors.white, + hasShadow: true, + shadowColor: Colors.black, + shadowBlurRadius: 4.0, + shadowOffset: const Offset(2.0, 2.0), + ); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: Scaffold( + body: EditableTextWidget( + index: 0, + textItem: textItem, + isSelected: false, + ), + ), + ), + ), + ); + + final textWidget = tester.widget(find.text('Shadow Text')); + expect(textWidget.style?.shadows, isNotNull); + expect(textWidget.style?.shadows?.length, 1); + }); + + testWidgets('applies text alignment', (tester) async { + final textItem = TextItem( + text: 'Centered Text', + x: 10, + y: 20, + fontSize: 16, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontFamily: 'Roboto', + isUnderlined: false, + color: Colors.white, + textAlign: TextAlign.center, + ); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: Scaffold( + body: EditableTextWidget( + index: 0, + textItem: textItem, + isSelected: false, + ), + ), + ), + ), + ); + + final textWidget = tester.widget(find.text('Centered Text')); + expect(textWidget.textAlign, TextAlign.center); + }); + + testWidgets('opens edit dialog on tap', (tester) async { + final textItem = TextItem( + text: 'Tap Me', + x: 10, + y: 20, + fontSize: 16, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontFamily: 'Roboto', + isUnderlined: false, + color: Colors.white, + ); + + canvasCubit.emit(CanvasState.initial().copyWith( + textItems: [textItem], + )); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: Scaffold( + body: EditableTextWidget( + index: 0, + textItem: textItem, + isSelected: false, + ), + ), + ), + ), + ); + + await tester.tap(find.text('Tap Me')); + await tester.pumpAndSettle(); + + expect(find.text('Edit Text'), findsOneWidget); + expect(find.byType(TextFormField), findsOneWidget); + }); + }); + + group('BackgroundColorTray', () { + late CanvasCubit canvasCubit; + + setUp(() { + canvasCubit = CanvasCubit(); + }); + + tearDown(() { + canvasCubit.close(); + }); + + testWidgets('displays all background colors', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: const Scaffold( + body: BackgroundColorTray(), + ), + ), + ), + ); + + expect(find.text('Background Color'), findsOneWidget); + + // Should find color selection containers + final colorContainers = find.byType(GestureDetector); + expect(colorContainers, findsWidgets); + }); + + testWidgets('shows selected color with checkmark', (tester) async { + canvasCubit.emit(CanvasState.initial().copyWith( + backgroundColor: ColorConstants.backgroundDeepPurple, + )); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: const Scaffold( + body: BackgroundColorTray(), + ), + ), + ), + ); + + await tester.pump(); + + // Should show check icon for selected color + expect(find.byIcon(Icons.check), findsOneWidget); + }); + + testWidgets('changes background color on tap', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: const Scaffold( + body: BackgroundColorTray(), + ), + ), + ), + ); + + await tester.pump(); + + final initialColor = canvasCubit.state.backgroundColor; + + // Find all GestureDetectors + final gestureDetectors = find.byType(GestureDetector); + final count = tester.widgetList(gestureDetectors).length; + + // Try tapping different gesture detectors until we find one that changes the color + bool colorChanged = false; + for (int i = 0; i < count && !colorChanged; i++) { + await tester.tap(gestureDetectors.at(i)); + await tester.pump(); + + if (canvasCubit.state.backgroundColor != initialColor) { + colorChanged = true; + } + } + + // At least one tap should have changed the color + expect(colorChanged, isTrue); + expect(canvasCubit.state.backgroundColor, isNot(equals(initialColor))); + }); }); - testWidgets('App has correct title', (WidgetTester tester) async { - // Build our app - await tester.pumpWidget(const MyApp()); + group('BackgroundOptionsSheet', () { + late CanvasCubit canvasCubit; + + setUp(() { + canvasCubit = CanvasCubit(); + }); + + tearDown(() { + canvasCubit.close(); + }); + + testWidgets('displays all background options', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: const Scaffold( + body: BackgroundOptionsSheet(), + ), + ), + ), + ); + + expect(find.text('Background Options'), findsOneWidget); + expect(find.text('Upload Image'), findsOneWidget); + expect(find.text('Take Photo'), findsOneWidget); + expect(find.text('Solid Color'), findsOneWidget); + expect(find.text('Remove Image'), findsOneWidget); + }); + + testWidgets('upload image option is always enabled', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: const Scaffold( + body: BackgroundOptionsSheet(), + ), + ), + ), + ); + + final uploadTile = find.ancestor( + of: find.text('Upload Image'), + matching: find.byType(InkWell), + ); + + expect(uploadTile, findsOneWidget); + }); + + testWidgets('remove image is disabled when no background image', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: const Scaffold( + body: BackgroundOptionsSheet(), + ), + ), + ), + ); + + await tester.pump(); + + // Remove Image should be visible but might be disabled + expect(find.text('Remove Image'), findsOneWidget); + }); - // Get the MaterialApp widget - final MaterialApp app = tester.widget(find.byType(MaterialApp)); + testWidgets('remove image is enabled when background image exists', + (tester) async { + canvasCubit.emit(CanvasState.initial().copyWith( + backgroundImagePath: '/fake/path/image.png', + )); - // Verify the title - expect(app.title, 'Text Editor'); + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: const Scaffold( + body: BackgroundOptionsSheet(), + ), + ), + ), + ); - // Wait for all timers and animations to complete - await tester.pumpAndSettle(); + await tester.pump(); + + expect(find.text('Remove Image'), findsOneWidget); + }); + + testWidgets('displays correct icons for each option', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: canvasCubit, + child: const Scaffold( + body: BackgroundOptionsSheet(), + ), + ), + ), + ); + + expect(find.byIcon(Icons.photo_library), findsOneWidget); + expect(find.byIcon(Icons.camera_alt), findsOneWidget); + expect(find.byIcon(Icons.color_lens), findsOneWidget); + expect(find.byIcon(Icons.clear), findsOneWidget); + }); }); - testWidgets('App has BlocProvider for CanvasCubit', - (WidgetTester tester) async { - // Build our app - await tester.pumpWidget(const MyApp()); + group('EditTextDialog', () { + testWidgets('displays with initial text', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: EditTextDialog(initialText: 'Initial Text'), + ), + ), + ); + + expect(find.text('Edit Text'), findsOneWidget); + expect(find.text('Initial Text'), findsOneWidget); + expect(find.text('Remove'), findsOneWidget); + expect(find.text('Save'), findsOneWidget); + }); + + testWidgets('validates empty text', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: EditTextDialog(initialText: 'Test'), + ), + ), + ); + + // Clear the text field + await tester.enterText(find.byType(TextFormField), ''); + + // Try to save + await tester.tap(find.text('Save')); + await tester.pumpAndSettle(); + + // Should show validation error + expect(find.text('Text cannot be empty'), findsOneWidget); + }); + + testWidgets('saves text on save button tap', (tester) async { + String? savedText; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + return ElevatedButton( + onPressed: () async { + final result = await showDialog( + context: context, + builder: (context) => const EditTextDialog( + initialText: 'Initial', + ), + ); + savedText = result; + }, + child: const Text('Open Dialog'), + ); + }, + ), + ), + ), + ); + + await tester.tap(find.text('Open Dialog')); + await tester.pumpAndSettle(); + + await tester.enterText(find.byType(TextFormField), 'Updated Text'); + await tester.tap(find.text('Save')); + await tester.pumpAndSettle(); + + expect(savedText, 'Updated Text'); + }); + + testWidgets('returns delete signal on remove button tap', (tester) async { + String? result; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + return ElevatedButton( + onPressed: () async { + result = await showDialog( + context: context, + builder: (context) => const EditTextDialog( + initialText: 'Test', + ), + ); + }, + child: const Text('Open Dialog'), + ); + }, + ), + ), + ), + ); + + await tester.tap(find.text('Open Dialog')); + await tester.pumpAndSettle(); - // Verify that BlocProvider is present in the widget tree - expect(find.byType(BlocProvider), findsOneWidget); + await tester.tap(find.text('Remove')); + await tester.pumpAndSettle(); - // Wait for all timers and animations to complete - await tester.pumpAndSettle(); + expect(result, '_delete_'); + }); }); } From f219810b198d35724f306b7a493a6cbcedc2042a Mon Sep 17 00:00:00 2001 From: preetidas60 Date: Mon, 20 Oct 2025 16:49:48 +0530 Subject: [PATCH 3/4] Structured the test directory --- lib/{tests => test/cubit}/canvas_cubit_test.dart | 0 lib/{tests => test/models}/draw_model_test.dart | 0 lib/{tests => test/models}/text_item_model_test.dart | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename lib/{tests => test/cubit}/canvas_cubit_test.dart (100%) rename lib/{tests => test/models}/draw_model_test.dart (100%) rename lib/{tests => test/models}/text_item_model_test.dart (100%) diff --git a/lib/tests/canvas_cubit_test.dart b/lib/test/cubit/canvas_cubit_test.dart similarity index 100% rename from lib/tests/canvas_cubit_test.dart rename to lib/test/cubit/canvas_cubit_test.dart diff --git a/lib/tests/draw_model_test.dart b/lib/test/models/draw_model_test.dart similarity index 100% rename from lib/tests/draw_model_test.dart rename to lib/test/models/draw_model_test.dart diff --git a/lib/tests/text_item_model_test.dart b/lib/test/models/text_item_model_test.dart similarity index 100% rename from lib/tests/text_item_model_test.dart rename to lib/test/models/text_item_model_test.dart From f76df9eda430214681521b6e7100f5cd230f6960 Mon Sep 17 00:00:00 2001 From: preetidas60 Date: Mon, 20 Oct 2025 16:52:35 +0530 Subject: [PATCH 4/4] Structured the test directory --- {lib/test => test}/cubit/canvas_cubit_test.dart | 0 {lib/test => test}/models/draw_model_test.dart | 0 {lib/test => test}/models/text_item_model_test.dart | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename {lib/test => test}/cubit/canvas_cubit_test.dart (100%) rename {lib/test => test}/models/draw_model_test.dart (100%) rename {lib/test => test}/models/text_item_model_test.dart (100%) diff --git a/lib/test/cubit/canvas_cubit_test.dart b/test/cubit/canvas_cubit_test.dart similarity index 100% rename from lib/test/cubit/canvas_cubit_test.dart rename to test/cubit/canvas_cubit_test.dart diff --git a/lib/test/models/draw_model_test.dart b/test/models/draw_model_test.dart similarity index 100% rename from lib/test/models/draw_model_test.dart rename to test/models/draw_model_test.dart diff --git a/lib/test/models/text_item_model_test.dart b/test/models/text_item_model_test.dart similarity index 100% rename from lib/test/models/text_item_model_test.dart rename to test/models/text_item_model_test.dart