diff --git a/README.md b/README.md index e67fafc..f758057 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,38 @@ All three methods work: If the DOCX file path is empty, the file type is unsupported, or the file doesn't exist, an error message will be displayed. If you provide an `onError` callback, it will be invoked with the error. +## Testing + +This package includes comprehensive test coverage to ensure reliability across all platforms. The test suite covers: + +- Text extraction from DOCX files (various formats, edge cases) +- Widget functionality (loading states, error handling, display) +- Platform-specific file I/O implementations +- Error handling and edge cases + +### Running Tests + +```bash +# Run all tests +flutter test + +# Run tests with coverage +flutter test --coverage + +# Use the provided test runner script +./test/run_tests.sh +``` + +### Continuous Integration + +Tests are automatically run on every pull request through GitHub Actions. The CI pipeline: +- Runs static analysis and formatting checks +- Executes all tests with coverage reporting +- Posts coverage reports as PR comments +- Ensures code quality standards are met + +For more details about the test suite, see [test/README.md](test/README.md). + ## Contributing We welcome contributions! If you'd like to contribute to this Flutter Package Project, please check out our [Contribution Guidelines](Contribution.md). diff --git a/lib/src/docx_view.dart b/lib/src/docx_view.dart index 448fed3..2c8c9a1 100644 --- a/lib/src/docx_view.dart +++ b/lib/src/docx_view.dart @@ -194,12 +194,15 @@ class _DocxViewState extends State { /// Handles errors by calling the [onError] callback if provided, or displaying the error message. void _handleError(Exception error) { + setState(() { + isLoading = false; + }); + if (widget.onError != null) { widget.onError!(error); } else { setState(() { fileContent = error.toString(); - isLoading = false; }); } } diff --git a/lib/src/extract_text_from_docx.dart b/lib/src/extract_text_from_docx.dart index 1905486..339b26d 100644 --- a/lib/src/extract_text_from_docx.dart +++ b/lib/src/extract_text_from_docx.dart @@ -43,7 +43,7 @@ String extractTextFromDocxBytes(Uint8List bytes) { paragraph.findAllElements('w:t').map((node) => node.innerText).join(); // Check for numbering information in the paragraph - final numIdNode = paragraph.findElements('w:numId').firstOrNull; + final numIdNode = paragraph.findAllElements('w:numId').firstOrNull; final numId = numIdNode?.getAttribute('w:val'); // Manage numbering: increment or reset based on numId changes diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..8172ac0 --- /dev/null +++ b/test/README.md @@ -0,0 +1,163 @@ +# Test Suite Documentation + +This directory contains comprehensive test coverage for the `docx_viewer` package. + +## Test Structure + +``` +test/ +├── docx_viewer_test.dart # Main test entry point +├── fixtures/ +│ └── test_docx_generator.dart # Helper to generate test DOCX files +├── src/ +│ ├── docx_view_test.dart # Widget tests for DocxView +│ ├── extract_text_from_docx_test.dart # Tests for text extraction +│ ├── file_io_stub_test.dart # Tests for stub implementation +│ └── file_io_web_test.dart # Tests for web implementation +└── utils/ + └── support_type_test.dart # Tests for utility classes +``` + +## Test Coverage + +### 1. Text Extraction Tests (`src/extract_text_from_docx_test.dart`) +- Extract text from simple DOCX files +- Extract text from DOCX with multiple paragraphs +- Handle empty DOCX documents +- Extract and number items from DOCX with numbering +- Handle special characters and unicode +- Handle invalid ZIP/DOCX data +- Handle empty paragraphs and whitespace +- Handle long text content +- Test FirstOrNullExtension utility + +### 2. DocxView Widget Tests (`src/docx_view_test.dart`) +- Display loading indicator during content load +- Display content after loading with bytes parameter +- Apply custom font size +- Use default font size when not specified +- Display multiple paragraphs with newlines +- Handle empty documents +- Call onError callback when no input provided +- Display error messages without callback +- Validate error when both filePath and bytes provided +- Handle invalid bytes gracefully +- Render content in scrollable view +- Handle numbered lists +- Apply correct padding + +### 3. Platform-Specific File I/O Tests +- Stub implementation tests (`src/file_io_stub_test.dart`) +- Web implementation tests (`src/file_io_web_test.dart`) +- Verify proper error messages for unsupported operations + +### 4. Utility Tests (`utils/support_type_test.dart`) +- Validate Supporttype constants + +## Running Tests + +### Run All Tests +```bash +flutter test +``` + +### Run Tests with Coverage +```bash +flutter test --coverage +``` + +### Run Specific Test File +```bash +flutter test test/src/docx_view_test.dart +``` + +### View Coverage Report +After running tests with coverage, you can generate an HTML report: + +```bash +# Install lcov (Ubuntu/Debian) +sudo apt-get install lcov + +# Generate HTML report +genhtml coverage/lcov.info -o coverage/html + +# Open in browser +open coverage/html/index.html +``` + +## Test Fixtures + +The `fixtures/test_docx_generator.dart` file provides helper methods to generate test DOCX files: + +- `createSimpleDocx(String text)` - Creates a simple DOCX with given text +- `createDocxWithNumbering(List items)` - Creates DOCX with numbered list +- `createEmptyDocx()` - Creates an empty DOCX file +- `createDocxWithMultipleParagraphs(List paragraphs)` - Creates DOCX with multiple paragraphs + +These helpers create proper DOCX files (ZIP archives) with the correct XML structure for testing. + +## Continuous Integration + +Tests are automatically run on every pull request through GitHub Actions (`.github/workflows/ci.yml`): + +1. **Analyze Job**: Runs static analysis and formatting checks +2. **Test Job**: Runs all tests with coverage + - Generates coverage report + - Posts coverage summary as PR comment + - Uploads coverage artifacts + +The CI workflow: +- Runs on pull requests to `main` and `dev` branches +- Runs on push to `main` and `dev` branches +- Generates test coverage reports +- Comments on PRs with coverage information +- Provides coverage badges + +## Adding New Tests + +When adding new tests: + +1. Create test files in appropriate directories (`src/`, `utils/`, etc.) +2. Follow the existing test structure and naming conventions +3. Use descriptive test names that explain what is being tested +4. Include arrange-act-assert comments in tests for clarity +5. Import the test file in `docx_viewer_test.dart` to include in the main test suite +6. Run tests locally before committing + +Example: +```dart +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('FeatureName', () { + test('should do something specific', () { + // Arrange + final input = 'test'; + + // Act + final result = functionUnderTest(input); + + // Assert + expect(result, equals('expected')); + }); + }); +} +``` + +## Test Best Practices + +1. **Isolation**: Each test should be independent and not rely on other tests +2. **Clarity**: Use descriptive test names that explain the scenario +3. **Coverage**: Aim for high coverage but focus on meaningful tests +4. **Edge Cases**: Test boundary conditions, error cases, and edge cases +5. **Maintainability**: Keep tests simple and maintainable +6. **Performance**: Tests should run quickly to support rapid development + +## Coverage Goals + +The package aims for: +- **Minimum**: 80% code coverage +- **Target**: 90%+ code coverage +- **Focus**: All critical paths and error handling must be tested + +Current coverage is tracked automatically in CI and reported on pull requests. diff --git a/test/docx_viewer_test.dart b/test/docx_viewer_test.dart index 3721dde..8dcde94 100644 --- a/test/docx_viewer_test.dart +++ b/test/docx_viewer_test.dart @@ -1,22 +1,23 @@ -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:docx_viewer/docx_viewer.dart'; +// Import all test suites +import 'src/extract_text_from_docx_test.dart' as extract_text_tests; +import 'src/docx_view_test.dart' as docx_view_tests; +import 'src/file_io_stub_test.dart' as file_io_stub_tests; +import 'src/file_io_web_test.dart' as file_io_web_tests; +import 'utils/support_type_test.dart' as support_type_tests; +/// Main test file that runs all test suites for the docx_viewer package +/// +/// This ensures comprehensive test coverage across all components: +/// - Text extraction from DOCX files +/// - DocxView widget functionality +/// - Platform-specific file I/O implementations +/// - Utility classes void main() { - test('adds one to input values', () { - DocxView( - filePath: 'docs/sample.docx', - onError: (error) { - debugPrint(error.toString()); - }, - ); - - DocxView( - filePath: 'docs/sample.pdf', - onError: (error) { - debugPrint(error.toString()); - }, - ); - }); + group('Extract Text from DOCX Tests', extract_text_tests.main); + group('DocxView Widget Tests', docx_view_tests.main); + group('FileIO Stub Tests', file_io_stub_tests.main); + group('FileIO Web Tests', file_io_web_tests.main); + group('Support Type Tests', support_type_tests.main); } diff --git a/test/fixtures/test_docx_generator.dart b/test/fixtures/test_docx_generator.dart new file mode 100644 index 0000000..63cdc1e --- /dev/null +++ b/test/fixtures/test_docx_generator.dart @@ -0,0 +1,152 @@ +import 'dart:typed_data'; +import 'package:archive/archive.dart'; +import 'dart:convert'; + +/// Helper class to generate test DOCX files for testing purposes +class TestDocxGenerator { + /// Creates a simple DOCX file with the given text content + static Uint8List createSimpleDocx(String text) { + final archive = Archive(); + + // Create the required DOCX structure + _addContentTypes(archive); + _addRels(archive); + _addDocument(archive, text); + + // Encode the archive as a ZIP file + final zipEncoder = ZipEncoder(); + final zipBytes = zipEncoder.encode(archive); + return Uint8List.fromList(zipBytes); + } + + /// Creates a DOCX file with numbered list + static Uint8List createDocxWithNumbering(List items) { + final archive = Archive(); + + // Create the required DOCX structure + _addContentTypes(archive); + _addRels(archive); + _addDocumentWithNumbering(archive, items); + + // Encode the archive as a ZIP file + final zipEncoder = ZipEncoder(); + final zipBytes = zipEncoder.encode(archive); + return Uint8List.fromList(zipBytes); + } + + /// Creates an empty DOCX file + static Uint8List createEmptyDocx() { + return createSimpleDocx(''); + } + + /// Creates a DOCX file with multiple paragraphs + static Uint8List createDocxWithMultipleParagraphs(List paragraphs) { + final archive = Archive(); + + // Create the required DOCX structure + _addContentTypes(archive); + _addRels(archive); + _addDocumentWithParagraphs(archive, paragraphs); + + // Encode the archive as a ZIP file + final zipEncoder = ZipEncoder(); + final zipBytes = zipEncoder.encode(archive); + return Uint8List.fromList(zipBytes); + } + + static void _addContentTypes(Archive archive) { + const contentTypesXml = + ''' + + + + +'''; + + final file = ArchiveFile('[Content_Types].xml', contentTypesXml.length, + utf8.encode(contentTypesXml)); + archive.addFile(file); + } + + static void _addRels(Archive archive) { + const relsXml = ''' + + +'''; + + final file = + ArchiveFile('_rels/.rels', relsXml.length, utf8.encode(relsXml)); + archive.addFile(file); + } + + static void _addDocument(Archive archive, String text) { + final documentXml = + ''' + + + + + $text + + + +'''; + + final file = ArchiveFile( + 'word/document.xml', documentXml.length, utf8.encode(documentXml)); + archive.addFile(file); + } + + static void _addDocumentWithNumbering(Archive archive, List items) { + final paragraphs = items.asMap().entries.map((entry) { + return ''' + + + + + + + + + ${entry.value} + + '''; + }).join('\n'); + + final documentXml = + ''' + + +$paragraphs + +'''; + + final file = ArchiveFile( + 'word/document.xml', documentXml.length, utf8.encode(documentXml)); + archive.addFile(file); + } + + static void _addDocumentWithParagraphs( + Archive archive, List paragraphs) { + final paragraphsXml = paragraphs.map((text) { + return ''' + + + $text + + '''; + }).join('\n'); + + final documentXml = + ''' + + +$paragraphsXml + +'''; + + final file = ArchiveFile( + 'word/document.xml', documentXml.length, utf8.encode(documentXml)); + archive.addFile(file); + } +} diff --git a/test/run_tests.sh b/test/run_tests.sh new file mode 100755 index 0000000..8c24451 --- /dev/null +++ b/test/run_tests.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# Test runner script for docx_viewer package +# This script helps run tests locally and in CI/CD + +set -e + +echo "Running tests for docx_viewer package..." +echo "" + +# Check if Flutter is installed +if ! command -v flutter &> /dev/null; then + echo "❌ Flutter is not installed. Please install Flutter first." + echo " Visit: https://flutter.dev/docs/get-started/install" + exit 1 +fi + +# Print Flutter version +echo "Flutter version:" +flutter --version +echo "" + +# Get dependencies +echo "Installing dependencies..." +flutter pub get +echo "" + +# Run analyzer +echo "Running analyzer..." +flutter analyze +echo "" + +# Run tests with coverage +echo "Running tests with coverage..." +flutter test --coverage +echo "" + +# Check if lcov is available for coverage report +if command -v lcov &> /dev/null; then + echo "Generating coverage report..." + + # Generate summary + lcov --summary coverage/lcov.info + + # Generate HTML report + genhtml coverage/lcov.info -o coverage/html + + echo "" + echo "Coverage report generated at: coverage/html/index.html" + echo " Open with: open coverage/html/index.html (macOS) or xdg-open coverage/html/index.html (Linux)" +else + echo "lcov not installed. Skipping HTML coverage report generation." + echo " Install with: sudo apt-get install lcov (Ubuntu/Debian) or brew install lcov (macOS)" +fi + +echo "" +echo "✅ All tests completed successfully!" diff --git a/test/src/docx_view_test.dart b/test/src/docx_view_test.dart new file mode 100644 index 0000000..a1b8478 --- /dev/null +++ b/test/src/docx_view_test.dart @@ -0,0 +1,343 @@ +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:docx_viewer/docx_viewer.dart'; +import '../fixtures/test_docx_generator.dart'; + +void main() { + group('DocxView Widget', () { + testWidgets('should display loading indicator initially', + (WidgetTester tester) async { + // Arrange + final docxBytes = TestDocxGenerator.createSimpleDocx('Test content'); + + // Act + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DocxView(bytes: docxBytes), + ), + ), + ); + + // Assert + expect(find.byType(CircularProgressIndicator), findsOneWidget); + }); + + testWidgets('should display content after loading with bytes parameter', + (WidgetTester tester) async { + // Arrange + final docxBytes = TestDocxGenerator.createSimpleDocx('Test content'); + + // Act + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DocxView(bytes: docxBytes), + ), + ), + ); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('Test content'), findsOneWidget); + expect(find.byType(CircularProgressIndicator), findsNothing); + }); + + testWidgets('should apply custom font size', (WidgetTester tester) async { + // Arrange + final docxBytes = TestDocxGenerator.createSimpleDocx('Test content'); + const customFontSize = 24; + + // Act + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DocxView( + bytes: docxBytes, + fontSize: customFontSize, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + // Assert + final textWidget = tester.widget(find.text('Test content')); + expect(textWidget.style?.fontSize, equals(customFontSize.toDouble())); + }); + + testWidgets('should use default font size when not specified', + (WidgetTester tester) async { + // Arrange + final docxBytes = TestDocxGenerator.createSimpleDocx('Test content'); + + // Act + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DocxView(bytes: docxBytes), + ), + ), + ); + await tester.pumpAndSettle(); + + // Assert + final textWidget = tester.widget(find.text('Test content')); + expect(textWidget.style?.fontSize, equals(16.0)); // Default font size + }); + + testWidgets('should display multiple paragraphs with newlines', + (WidgetTester tester) async { + // Arrange + final paragraphs = ['First line', 'Second line', 'Third line']; + final docxBytes = + TestDocxGenerator.createDocxWithMultipleParagraphs(paragraphs); + + // Act + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DocxView(bytes: docxBytes), + ), + ), + ); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('First line\nSecond line\nThird line'), findsOneWidget); + }); + + testWidgets('should handle empty document', (WidgetTester tester) async { + // Arrange + final docxBytes = TestDocxGenerator.createEmptyDocx(); + + // Act + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DocxView(bytes: docxBytes), + ), + ), + ); + await tester.pumpAndSettle(); + + // Assert + expect(find.text(''), findsOneWidget); + }); + + testWidgets('should call onError callback when no input provided', + (WidgetTester tester) async { + // Arrange + Exception? capturedError; + void onErrorCallback(Exception error) { + capturedError = error; + } + + // Act + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DocxView( + onError: onErrorCallback, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + // Assert + expect(capturedError, isNotNull); + expect( + capturedError.toString(), + contains('No input provided'), + ); + }); + + testWidgets( + 'should display error message when no input provided and no callback', + (WidgetTester tester) async { + // Act + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DocxView(), + ), + ), + ); + await tester.pumpAndSettle(); + + // Assert + expect(find.textContaining('No input provided'), findsOneWidget); + }); + + testWidgets('should call onError when both filePath and bytes are provided', + (WidgetTester tester) async { + // Arrange + final docxBytes = TestDocxGenerator.createSimpleDocx('Test'); + Exception? capturedError; + void onErrorCallback(Exception error) { + capturedError = error; + } + + // Act + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DocxView( + filePath: 'test.docx', + bytes: docxBytes, + onError: onErrorCallback, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + // Assert + expect(capturedError, isNotNull); + expect( + capturedError.toString(), + contains('Define only one of'), + ); + }); + + testWidgets( + 'should display error when both filePath and bytes provided without callback', + (WidgetTester tester) async { + // Arrange + final docxBytes = TestDocxGenerator.createSimpleDocx('Test'); + + // Act + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DocxView( + filePath: 'test.docx', + bytes: docxBytes, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + // Assert + expect(find.textContaining('Define only one of'), findsOneWidget); + }); + + testWidgets('should handle invalid bytes gracefully', + (WidgetTester tester) async { + // Arrange + final invalidBytes = Uint8List.fromList([1, 2, 3, 4, 5]); + Exception? capturedError; + + // Act + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DocxView( + bytes: invalidBytes, + onError: (error) { + capturedError = error; + }, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + // Assert + expect(capturedError, isNotNull); + expect(capturedError.toString(), contains('Error reading file')); + }); + + testWidgets('should render content in a scrollable view', + (WidgetTester tester) async { + // Arrange + final docxBytes = TestDocxGenerator.createDocxWithMultipleParagraphs( + List.generate(100, (i) => 'Line $i'), + ); + + // Act + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DocxView(bytes: docxBytes), + ), + ), + ); + await tester.pumpAndSettle(); + + // Assert + expect(find.byType(SingleChildScrollView), findsOneWidget); + }); + + testWidgets( + 'should display "No content to display" when fileContent is null', + (WidgetTester tester) async { + // This test ensures the fallback message is shown + // We test this indirectly by checking the default display + + // Arrange + final docxBytes = TestDocxGenerator.createEmptyDocx(); + + // Act + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DocxView(bytes: docxBytes), + ), + ), + ); + await tester.pumpAndSettle(); + + // Assert - empty content should just show empty text, not "No content to display" + expect(find.byType(Text), findsWidgets); + }); + + testWidgets('should handle numbered lists correctly', + (WidgetTester tester) async { + // Arrange + final items = ['First item', 'Second item', 'Third item']; + final docxBytes = TestDocxGenerator.createDocxWithNumbering(items); + + // Act + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DocxView(bytes: docxBytes), + ), + ), + ); + await tester.pumpAndSettle(); + + // Assert + final textFinder = find.byType(Text); + expect(textFinder, findsWidgets); + + final textWidget = tester.widget(textFinder.first); + expect(textWidget.data, contains('1. First item')); + expect(textWidget.data, contains('2. Second item')); + expect(textWidget.data, contains('3. Third item')); + }); + + testWidgets('should apply correct padding', (WidgetTester tester) async { + // Arrange + final docxBytes = TestDocxGenerator.createSimpleDocx('Test content'); + + // Act + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DocxView(bytes: docxBytes), + ), + ), + ); + await tester.pumpAndSettle(); + + // Assert + final container = tester.widget(find.byType(Container).first); + expect(container.padding, equals(const EdgeInsets.all(10.0))); + }); + }); +} diff --git a/test/src/extract_text_from_docx_test.dart b/test/src/extract_text_from_docx_test.dart new file mode 100644 index 0000000..1dabe72 --- /dev/null +++ b/test/src/extract_text_from_docx_test.dart @@ -0,0 +1,183 @@ +import 'dart:typed_data'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:docx_viewer/src/extract_text_from_docx.dart'; +import '../fixtures/test_docx_generator.dart'; + +void main() { + group('extractTextFromDocxBytes', () { + test('should extract text from simple DOCX with single paragraph', () { + // Arrange + final docxBytes = TestDocxGenerator.createSimpleDocx('Hello World'); + + // Act + final result = extractTextFromDocxBytes(docxBytes); + + // Assert + expect(result, equals('Hello World')); + }); + + test('should extract text from DOCX with multiple paragraphs', () { + // Arrange + final paragraphs = [ + 'First paragraph', + 'Second paragraph', + 'Third paragraph' + ]; + final docxBytes = + TestDocxGenerator.createDocxWithMultipleParagraphs(paragraphs); + + // Act + final result = extractTextFromDocxBytes(docxBytes); + + // Assert + expect( + result, equals('First paragraph\nSecond paragraph\nThird paragraph')); + }); + + test('should handle empty DOCX document', () { + // Arrange + final docxBytes = TestDocxGenerator.createEmptyDocx(); + + // Act + final result = extractTextFromDocxBytes(docxBytes); + + // Assert + expect(result, equals('')); + }); + + test('should extract and number items from DOCX with numbering', () { + // Arrange + final items = ['First item', 'Second item', 'Third item']; + final docxBytes = TestDocxGenerator.createDocxWithNumbering(items); + + // Act + final result = extractTextFromDocxBytes(docxBytes); + + // Assert + expect(result, contains('1. First item')); + expect(result, contains('2. Second item')); + expect(result, contains('3. Third item')); + }); + + test('should handle DOCX with special characters', () { + // Arrange + final text = 'Special chars: @#\$%^&*()'; + final docxBytes = TestDocxGenerator.createSimpleDocx(text); + + // Act + final result = extractTextFromDocxBytes(docxBytes); + + // Assert + expect(result, equals(text)); + }); + + test('should handle DOCX with unicode characters', () { + // Arrange + final text = 'Unicode: 你好 مرحبا שלום'; + final docxBytes = TestDocxGenerator.createSimpleDocx(text); + + // Act + final result = extractTextFromDocxBytes(docxBytes); + + // Assert + expect(result, equals(text)); + }); + + test('should throw exception when document.xml is not found', () { + // Arrange + final invalidBytes = + Uint8List.fromList([80, 75, 3, 4]); // ZIP header but invalid DOCX + + // Act & Assert + expect( + () => extractTextFromDocxBytes(invalidBytes), + throwsA(isA()), + ); + }); + + test('should throw exception when bytes are not valid ZIP', () { + // Arrange + final invalidBytes = Uint8List.fromList([1, 2, 3, 4, 5]); + + // Act & Assert + expect( + () => extractTextFromDocxBytes(invalidBytes), + throwsException, + ); + }); + + test('should handle DOCX with empty paragraphs between text', () { + // Arrange + final paragraphs = ['First', '', 'Third']; + final docxBytes = + TestDocxGenerator.createDocxWithMultipleParagraphs(paragraphs); + + // Act + final result = extractTextFromDocxBytes(docxBytes); + + // Assert + expect(result, equals('First\n\nThird')); + }); + + test('should handle DOCX with only whitespace', () { + // Arrange + final docxBytes = TestDocxGenerator.createSimpleDocx(' '); + + // Act + final result = extractTextFromDocxBytes(docxBytes); + + // Assert + expect(result, equals(' ')); + }); + + test('should handle DOCX with long text', () { + // Arrange + final longText = 'A' * 1000; + final docxBytes = TestDocxGenerator.createSimpleDocx(longText); + + // Act + final result = extractTextFromDocxBytes(docxBytes); + + // Assert + expect(result.length, equals(1000)); + expect(result, equals(longText)); + }); + + test('should handle DOCX with newline characters in text', () { + // Arrange + final text = 'Line 1\nLine 2'; + final docxBytes = TestDocxGenerator.createSimpleDocx(text); + + // Act + final result = extractTextFromDocxBytes(docxBytes); + + // Assert + expect(result, contains('Line 1')); + expect(result, contains('Line 2')); + }); + }); + + group('FirstOrNullExtension', () { + test('should return first element when iterable is not empty', () { + // Arrange + final list = [1, 2, 3]; + + // Act + final result = list.firstOrNull; + + // Assert + expect(result, equals(1)); + }); + + test('should return null when iterable is empty', () { + // Arrange + final list = []; + + // Act + final result = list.firstOrNull; + + // Assert + expect(result, isNull); + }); + }); +} diff --git a/test/src/file_io_stub_test.dart b/test/src/file_io_stub_test.dart new file mode 100644 index 0000000..af46452 --- /dev/null +++ b/test/src/file_io_stub_test.dart @@ -0,0 +1,52 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:docx_viewer/src/file_io_stub.dart'; + +void main() { + group('FileIO Stub Implementation', () { + test('readFileBytes should throw UnsupportedError', () async { + // Act & Assert + expect( + () async => await FileIO.readFileBytes('test.docx'), + throwsA(isA()), + ); + }); + + test('fileExists should throw UnsupportedError', () async { + // Act & Assert + expect( + () async => await FileIO.fileExists('test.docx'), + throwsA(isA()), + ); + }); + + test('readFileBytes error message should be descriptive', () async { + // Act & Assert + try { + await FileIO.readFileBytes('test.docx'); + fail('Should have thrown UnsupportedError'); + } catch (e) { + expect(e, isA()); + expect( + e.toString(), + contains( + 'Cannot read files without platform-specific implementation'), + ); + } + }); + + test('fileExists error message should be descriptive', () async { + // Act & Assert + try { + await FileIO.fileExists('test.docx'); + fail('Should have thrown UnsupportedError'); + } catch (e) { + expect(e, isA()); + expect( + e.toString(), + contains( + 'Cannot check file existence without platform-specific implementation'), + ); + } + }); + }); +} diff --git a/test/src/file_io_web_test.dart b/test/src/file_io_web_test.dart new file mode 100644 index 0000000..9fefec2 --- /dev/null +++ b/test/src/file_io_web_test.dart @@ -0,0 +1,74 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:docx_viewer/src/file_io_web.dart'; + +void main() { + group('FileIO Web Implementation', () { + test('readFileBytes should throw UnsupportedError', () async { + // Act & Assert + expect( + () async => await FileIO.readFileBytes('test.docx'), + throwsA(isA()), + ); + }); + + test('fileExists should throw UnsupportedError', () async { + // Act & Assert + expect( + () async => await FileIO.fileExists('test.docx'), + throwsA(isA()), + ); + }); + + test('readFileBytes error message should mention web platform limitation', + () async { + // Act & Assert + try { + await FileIO.readFileBytes('test.docx'); + fail('Should have thrown UnsupportedError'); + } catch (e) { + expect(e, isA()); + expect( + e.toString(), + contains('Direct file path access is not supported on web'), + ); + expect( + e.toString(), + contains('bytes'), + ); + } + }); + + test('fileExists error message should mention web platform limitation', + () async { + // Act & Assert + try { + await FileIO.fileExists('test.docx'); + fail('Should have thrown UnsupportedError'); + } catch (e) { + expect(e, isA()); + expect( + e.toString(), + contains('File system access is not supported on web'), + ); + } + }); + + test('readFileBytes should provide alternative solution in error', + () async { + // Act & Assert + try { + await FileIO.readFileBytes('test.docx'); + fail('Should have thrown UnsupportedError'); + } catch (e) { + expect( + e.toString(), + contains('file picker'), + ); + expect( + e.toString(), + contains('network URL'), + ); + } + }); + }); +} diff --git a/test/utils/support_type_test.dart b/test/utils/support_type_test.dart new file mode 100644 index 0000000..bc4b8a9 --- /dev/null +++ b/test/utils/support_type_test.dart @@ -0,0 +1,26 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:docx_viewer/utils/support_type.dart'; + +void main() { + group('Supporttype', () { + test('should have docx constant defined', () { + // Assert + expect(Supporttype.docx, equals('docx')); + }); + + test('docx constant should be lowercase', () { + // Assert + expect(Supporttype.docx, equals(Supporttype.docx.toLowerCase())); + }); + + test('docx constant should be a String', () { + // Assert + expect(Supporttype.docx, isA()); + }); + + test('docx constant should not be empty', () { + // Assert + expect(Supporttype.docx.isNotEmpty, isTrue); + }); + }); +}