test(blocs): Restore bloc property tests with @GenerateMocks scaffolding #14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Tests | |
| # ======================================================================== | |
| # Workflow Triggers Configuration | |
| # ======================================================================== | |
| on: | |
| push: | |
| branches: [ main ] | |
| pull_request: | |
| branches: [ main ] | |
| workflow_dispatch: | |
| # Cancel in-flight runs for the same ref when a newer commit arrives. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| test: | |
| name: Run Tests | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| channel: 'stable' | |
| cache: true | |
| - name: Prepare .env from template | |
| run: cp .env.example .env | |
| - name: Install dependencies | |
| run: flutter pub get | |
| - name: Verify formatting | |
| run: dart format --set-exit-if-changed . | |
| continue-on-error: false | |
| - name: Analyze code | |
| run: flutter analyze | |
| continue-on-error: false | |
| - name: Run tests | |
| timeout-minutes: 15 | |
| run: flutter test --coverage --timeout=5m | |
| continue-on-error: false | |
| # ======================================================================== | |
| # Coverage Processing - Critical Step | |
| # ======================================================================== | |
| - name: Install lcov | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y lcov | |
| - name: Remove generated files from coverage | |
| run: | | |
| # Remove generated and non-testable files to match codecov.yml | |
| lcov --remove coverage/lcov.info --ignore-errors unused \ | |
| '**/*.g.dart' \ | |
| '**/*.freezed.dart' \ | |
| '**/*.config.dart' \ | |
| '**/generated/**' \ | |
| '**/l10n/**' \ | |
| '**/main.dart' \ | |
| 'test/**' \ | |
| '**/*_test.dart' \ | |
| '**/test_helpers.dart' \ | |
| '**/test_fixtures.dart' \ | |
| -o coverage/lcov.info | |
| echo "✅ Removed generated files from coverage report" | |
| - name: Generate coverage report | |
| run: | | |
| genhtml coverage/lcov.info -o coverage/html --no-function-coverage | |
| - name: Calculate coverage | |
| id: coverage | |
| run: | | |
| COVERAGE=$(lcov --summary coverage/lcov.info 2>&1 | grep "lines.*:" | head -1 | grep -oP '\d+\.\d+%' | head -1 | sed 's/%//' || echo "0") | |
| echo "coverage=$COVERAGE" >> $GITHUB_OUTPUT | |
| echo "Coverage: ${COVERAGE}%" | |
| - name: Calculate coverage by layer | |
| id: layer_coverage | |
| run: | | |
| chmod +x scripts/linux/testing/calculate_layer_coverage.sh | |
| ./scripts/linux/testing/calculate_layer_coverage.sh coverage/lcov.info > /tmp/layer_coverage.txt | |
| # Read key=value pairs and write to GITHUB_OUTPUT | |
| while IFS='=' read -r key value; do | |
| echo "${key}=${value}" >> $GITHUB_OUTPUT | |
| done < /tmp/layer_coverage.txt | |
| cat /tmp/layer_coverage.txt | |
| # Coverage thresholds are disabled while the app is still in active | |
| # development (features land faster than tests right now). The job | |
| # below still measures coverage and reports it on PRs — re-enable the | |
| # gate once feature work stabilises by uncommenting this step. | |
| # | |
| # Targets when re-enabled: total 80%, domain 100%, data 90%, | |
| # presentation 80%, core 80%. | |
| # | |
| # - name: Check coverage threshold | |
| # run: | | |
| # COVERAGE=${{ steps.coverage.outputs.coverage }} | |
| # DOMAIN_COV=${{ steps.layer_coverage.outputs.domain }} | |
| # DATA_COV=${{ steps.layer_coverage.outputs.data }} | |
| # PRESENTATION_COV=${{ steps.layer_coverage.outputs.presentation }} | |
| # CORE_COV=${{ steps.layer_coverage.outputs.core }} | |
| # | |
| # echo "Checking thresholds..." | |
| # FAILED=0 | |
| # | |
| # check_threshold() { | |
| # local name=$1 | |
| # local current=$2 | |
| # local target=$3 | |
| # if (( $(echo "$current < $target" | bc -l) )); then | |
| # echo "❌ $name: ${current}% < ${target}%" | |
| # return 1 | |
| # else | |
| # echo "✅ $name: ${current}% >= ${target}%" | |
| # return 0 | |
| # fi | |
| # } | |
| # | |
| # check_threshold "Total Coverage" "$COVERAGE" 80 || exit 1 | |
| # check_threshold "Domain Layer" "$DOMAIN_COV" 100 || FAILED=1 | |
| # check_threshold "Data Layer" "$DATA_COV" 90 || FAILED=1 | |
| # check_threshold "Presentation Layer" "$PRESENTATION_COV" 80 || FAILED=1 | |
| # check_threshold "Core Layer" "$CORE_COV" 80 || FAILED=1 | |
| # | |
| # if [ $FAILED -eq 1 ]; then | |
| # echo "⚠️ One or more architectural layers did not meet coverage targets." | |
| # exit 1 | |
| # fi | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@v5 | |
| with: | |
| files: ./coverage/lcov.info | |
| flags: unittests | |
| name: codecov-umbrella | |
| fail_ci_if_error: false | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| - name: Upload coverage HTML report | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: coverage-report | |
| path: coverage/html | |
| retention-days: 30 | |
| - name: Comment PR with coverage | |
| if: github.event_name == 'pull_request' | |
| uses: marocchino/sticky-pull-request-comment@v2 | |
| with: | |
| recreate: true | |
| message: | | |
| ## 📊 Test Coverage Report | |
| **Coverage:** ${{ steps.coverage.outputs.coverage }}% | |
| **Status:** ${{ steps.coverage.outputs.coverage >= 80 && '✅ Passing' || '❌ Failing' }} | |
| Coverage report is available in the [workflow artifacts](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}). | |
| ### Coverage by Layer | |
| | Layer | Coverage | Target | Status | | |
| |-------|----------|--------|--------| | |
| | **Domain** | ${{ steps.layer_coverage.outputs.domain_display }} | 100% | ${{ steps.layer_coverage.outputs.domain >= 100 && '✅' || '❌' }} | | |
| | **Data** | ${{ steps.layer_coverage.outputs.data_display }} | 90% | ${{ steps.layer_coverage.outputs.data >= 90 && '✅' || '❌' }} | | |
| | **Presentation** | ${{ steps.layer_coverage.outputs.presentation_display }} | 80% | ${{ steps.layer_coverage.outputs.presentation >= 80 && '✅' || '❌' }} | | |
| | **Core** | ${{ steps.layer_coverage.outputs.core_display }} | 80% | ${{ steps.layer_coverage.outputs.core >= 80 && '✅' || '❌' }} | |