├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.yaml │ ├── build.yaml │ ├── chore.yaml │ ├── ci.yaml │ ├── config.yml │ ├── documentation.yaml │ ├── feature_request.yaml │ ├── performance.yaml │ ├── refactor.yaml │ ├── revert.yaml │ ├── style.yaml │ └── test.yaml ├── cspell.json ├── dependabot.yaml ├── pull_request_template.md └── workflows │ ├── example.yaml │ ├── main.yaml │ ├── pub_publish.yaml │ └── sync_labels.yaml ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── assets └── vgv_logo.png ├── coverage_badge.svg ├── example ├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── ventures │ │ │ │ │ └── verygood │ │ │ │ │ └── example │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable-v21 │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values-night │ │ │ │ └── styles.xml │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h ├── lib │ ├── app.dart │ ├── main.dart │ └── ui │ │ ├── home_screen.dart │ │ ├── pincode_screen.dart │ │ ├── quiz_dialog.dart │ │ └── ui.dart ├── pubspec.yaml ├── test │ ├── app_test.dart │ ├── helpers.dart │ └── ui │ │ ├── home_screen_test.dart │ │ ├── pincode_screen_test.dart │ │ └── quiz_dialog_test.dart └── web │ ├── favicon.png │ ├── icons │ ├── Icon-192.png │ └── Icon-512.png │ ├── index.html │ └── manifest.json ├── lib ├── mockingjay.dart └── src │ ├── matcher_extensions.dart │ ├── matchers.dart │ └── mock_navigator.dart ├── pubspec.yaml ├── test └── src │ ├── example_test.dart │ ├── matchers_test.dart │ └── mock_navigator_test.dart └── tool ├── release_ready.sh └── verify_pub_score.sh /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Every request must be reviewed and accepted by: 2 | 3 | * @VeryGoodOpenSource/codeowners -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a report to help us improve 3 | title: "fix: " 4 | labels: [bug] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: A clear and concise description of what the bug is. 11 | placeholder: "Describe the bug." 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: setps-to-reproduce 16 | attributes: 17 | label: Steps To Reproduce 18 | description: A set of instructions, step by step, explaining how to reproduce the bug. 19 | placeholder: | 20 | 1. Go to '...' 21 | 2. Click on '....' 22 | 3. Scroll down to '....' 23 | 4. See error 24 | validations: 25 | required: true 26 | - type: textarea 27 | id: expected-behavior 28 | attributes: 29 | label: Expected Behavior 30 | description: A clear and concise description of what you expected to happen. 31 | placeholder: "Describe what you expected to happen." 32 | validations: 33 | required: true 34 | - type: textarea 35 | id: additional-context 36 | attributes: 37 | label: Additional Context 38 | description: Add any other context, including links/screenshots/video recordings/etc about the problem here. 39 | placeholder: "Provide context here." 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build System 2 | description: Changes that affect the build system or external dependencies 3 | title: "build: " 4 | labels: [build] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: Describe what changes need to be done to the build system and why 11 | placeholder: "Describe the build system change." 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: requirements 16 | attributes: 17 | label: Requirements 18 | description: The list of requirements that need to be met in order to consider the ticket to be completed. Please be as explicit as possible. 19 | value: | 20 | - [ ] All CI/CD checks are passing. 21 | - [ ] There is no drop in the test coverage percentage. 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: additional-context 26 | attributes: 27 | label: Additional Context 28 | description: Add any other context, including links/screenshots/video recordings/etc about the problem here. 29 | placeholder: "Provide context here." 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/chore.yaml: -------------------------------------------------------------------------------- 1 | name: Chore 2 | description: Other changes that don't modify source or test files 3 | title: "chore: " 4 | labels: [chore] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: Clearly describe what change is needed and why. If this changes code then please use another issue type. 11 | placeholder: "Provide a description of the chore." 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: requirements 16 | attributes: 17 | label: Requirements 18 | description: The list of requirements that need to be met in order to consider the ticket to be completed. Please be as explicit as possible. 19 | value: | 20 | - [ ] No functional changes to the code. 21 | - [ ] All CI/CD checks are passing. 22 | - [ ] There is no drop in the test coverage percentage. 23 | validations: 24 | required: true 25 | - type: textarea 26 | id: additional-context 27 | attributes: 28 | label: Additional Context 29 | description: Add any other context, including links/screenshots/video recordings/etc about the problem here. 30 | placeholder: "Provide context here." 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | description: Changes to the CI configuration files and scripts 3 | title: "ci: " 4 | labels: [ci] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: Describe what changes need to be done to the CI/CD system and why. 11 | placeholder: "Provide a description of the changes that need to be done to the CI/CD system." 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: requirements 16 | attributes: 17 | label: Requirements 18 | description: The list of requirements that need to be met in order to consider the ticket to be completed. Please be as explicit as possible. 19 | value: | 20 | - [ ] All CI/CD checks are passing. 21 | - [ ] There is no drop in the test coverage percentage. 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: additional-context 26 | attributes: 27 | label: Additional Context 28 | description: Add any other context, including links/screenshots/video recordings/etc about the problem here. 29 | placeholder: "Provide context here." 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.yaml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | description: Improve the documentation so all collaborators have a common understanding 3 | title: "docs: " 4 | labels: [documentation] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: Clearly describe what documentation you are looking to add or improve. 11 | placeholder: "Provide a description of the documentation changes." 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: requirements 16 | attributes: 17 | label: Requirements 18 | description: The list of requirements that need to be met in order to consider the ticket to be completed. Please be as explicit as possible. 19 | value: | 20 | - [ ] No functional changes to the code. 21 | - [ ] All CI/CD checks are passing. 22 | - [ ] There is no drop in the test coverage percentage. 23 | validations: 24 | required: true 25 | - type: textarea 26 | id: additional-context 27 | attributes: 28 | label: Additional Context 29 | description: Add any other context, including links/screenshots/video recordings/etc about the problem here. 30 | placeholder: "Provide context here." 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: A new feature to be added to the project 3 | title: "feat: " 4 | labels: [feature] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: Clearly describe what you are looking to add. The more business/user context the better. 11 | placeholder: "Provide a description of the feature." 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: requirements 16 | attributes: 17 | label: Requirements 18 | description: The list of requirements that need to be met in order to consider the ticket to be completed. Please be as explicit as possible. 19 | value: | 20 | - [ ] All CI/CD checks are passing. 21 | - [ ] There is no drop in the test coverage percentage. 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: additional-context 26 | attributes: 27 | label: Additional Context 28 | description: Add any other context, including links/screenshots/video recordings/etc about the problem here. 29 | placeholder: "Provide context here." 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/performance.yaml: -------------------------------------------------------------------------------- 1 | name: Performance Update 2 | description: A code change that improves performance 3 | title: "perf: " 4 | labels: [performance] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: Clearly describe what code needs to be changed and what the performance impact is going to be. Bonus point's if you can tie this directly to user experience. 11 | placeholder: " Provide a description of the performance update." 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: requirements 16 | attributes: 17 | label: Requirements 18 | description: The list of requirements that need to be met in order to consider the ticket to be completed. Please be as explicit as possible. 19 | value: | 20 | - [ ] All CI/CD checks are passing. 21 | - [ ] There is no drop in the test coverage percentage. 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: additional-context 26 | attributes: 27 | label: Additional Context 28 | description: Add any other context, including links/screenshots/video recordings/etc about the problem here. 29 | placeholder: "Provide context here." 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/refactor.yaml: -------------------------------------------------------------------------------- 1 | name: Refactor 2 | description: A code change that neither fixes a bug nor adds a feature 3 | title: "refactor: " 4 | labels: [refactor] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: Clearly describe what needs to be refactored and why. Please provide links to related issues (bugs or upcoming features) in order to help prioritize. 11 | placeholder: "Provide a description of the refactor." 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: requirements 16 | attributes: 17 | label: Requirements 18 | description: The list of requirements that need to be met in order to consider the ticket to be completed. Please be as explicit as possible. 19 | value: | 20 | - [ ] All CI/CD checks are passing. 21 | - [ ] There is no drop in the test coverage percentage. 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: additional-context 26 | attributes: 27 | label: Additional Context 28 | description: Add any other context, including links/screenshots/video recordings/etc about the problem here. 29 | placeholder: "Provide context here." 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/revert.yaml: -------------------------------------------------------------------------------- 1 | name: Revert 2 | description: Revert a previous commit 3 | title: "revert: " 4 | labels: [revert] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: Provide a link to a PR/Commit that you are looking to revert and why. 11 | placeholder: "Provide a description of and link to the commit that needs to be reverted." 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: requirements 16 | attributes: 17 | label: Requirements 18 | description: The list of requirements that need to be met in order to consider the ticket to be completed. Please be as explicit as possible. 19 | value: | 20 | - [ ] Change has been reverted. 21 | - [ ] No change in unit/widget test coverage has happened. 22 | - [ ] A new ticket is created for any follow on work that needs to happen. 23 | validations: 24 | required: true 25 | - type: textarea 26 | id: additional-context 27 | attributes: 28 | label: Additional Context 29 | description: Add any other context, including links/screenshots/video recordings/etc about the problem here. 30 | placeholder: "Provide context here." 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/style.yaml: -------------------------------------------------------------------------------- 1 | name: Style 2 | description: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 3 | title: "style: " 4 | labels: [style] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: Clearly describe what you are looking to change and why. 11 | placeholder: "Provide a description of the style changes." 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: requirements 16 | attributes: 17 | label: Requirements 18 | description: The list of requirements that need to be met in order to consider the ticket to be completed. Please be as explicit as possible. 19 | value: | 20 | - [ ] All CI/CD checks are passing. 21 | - [ ] There is no drop in the unit or widget test coverage percentage. 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: additional-context 26 | attributes: 27 | label: Additional Context 28 | description: Add any other context, including links/screenshots/video recordings/etc about the problem here. 29 | placeholder: "Provide context here." 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | description: Adding missing tests or correcting existing tests 3 | title: "test: " 4 | labels: [test] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: List out the tests that need to be added or changed. Please also include any information as to why this was not covered in the past. 11 | placeholder: "Provide a description of the tests that need to be added or changed." 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: requirements 16 | attributes: 17 | label: Requirements 18 | description: The list of requirements that need to be met in order to consider the ticket to be completed. Please be as explicit as possible. 19 | value: | 20 | - [ ] All CI/CD checks are passing. 21 | - [ ] There is no drop in the test coverage percentage. 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: additional-context 26 | attributes: 27 | label: Additional Context 28 | description: Add any other context, including links/screenshots/video recordings/etc about the problem here. 29 | placeholder: "Provide context here." 30 | -------------------------------------------------------------------------------- /.github/cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", 4 | "dictionaries": [ 5 | "vgv_allowed", 6 | "vgv_forbidden" 7 | ], 8 | "dictionaryDefinitions": [ 9 | { 10 | "name": "vgv_allowed", 11 | "path": "https://raw.githubusercontent.com/verygoodopensource/very_good_dictionaries/main/allowed.txt", 12 | "description": "Allowed VGV Spellings" 13 | }, 14 | { 15 | "name": "vgv_forbidden", 16 | "path": "https://raw.githubusercontent.com/verygoodopensource/very_good_dictionaries/main/forbidden.txt", 17 | "description": "Forbidden VGV Spellings" 18 | } 19 | ], 20 | "useGitignore": true, 21 | "words": [ 22 | "Mockingjay", 23 | "korzonkiee", 24 | "Jignesh", 25 | "Fullscreen", 26 | "allisonryan" 27 | ] 28 | } -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "pub" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | - package-ecosystem: "pub" 12 | directory: "/example" 13 | schedule: 14 | interval: "daily" 15 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ## Status 10 | 11 | **READY/IN DEVELOPMENT/HOLD** 12 | 13 | ## Description 14 | 15 | 16 | 17 | ## Type of Change 18 | 19 | 20 | 21 | - [ ] ✨ New feature (non-breaking change which adds functionality) 22 | - [ ] 🛠️ Bug fix (non-breaking change which fixes an issue) 23 | - [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change) 24 | - [ ] 🧹 Code refactor 25 | - [ ] ✅ Build configuration change 26 | - [ ] 📝 Documentation 27 | - [ ] 🗑️ Chore 28 | -------------------------------------------------------------------------------- /.github/workflows/example.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | pull_request: 12 | branches: 13 | - main 14 | 15 | jobs: 16 | semantic_pull_request: 17 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1 18 | 19 | build: 20 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 21 | with: 22 | flutter_channel: stable 23 | flutter_version: "3.32.0" 24 | working_directory: example 25 | runs_on: macos-latest 26 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | pull_request: 12 | branches: 13 | - main 14 | 15 | jobs: 16 | semantic_pull_request: 17 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1 18 | 19 | build: 20 | strategy: 21 | matrix: 22 | flutter-version: 23 | # The version of Flutter to use should use the minimum Dart SDK version supported by the package, 24 | # refer to https://docs.flutter.dev/development/tools/sdk/releases. 25 | - "3.32.0" 26 | - "3.x" 27 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 28 | with: 29 | flutter_channel: stable 30 | flutter_version: ${{ matrix.flutter-version }} 31 | package_get_excludes: example 32 | 33 | spell-check: 34 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/spell_check.yml@v1 35 | with: 36 | includes: | 37 | **/*.{md,yaml} 38 | !.dart_tool/**/*.yaml 39 | .*/**/*.yml 40 | modified_files_only: false 41 | 42 | pana_score: 43 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/pana.yml@v1 44 | -------------------------------------------------------------------------------- /.github/workflows/pub_publish.yaml: -------------------------------------------------------------------------------- 1 | name: pub_publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v[0-9]+.[0-9]+.[0-9]+*" 7 | 8 | jobs: 9 | publish: 10 | permissions: 11 | id-token: write # Required for authentication using OIDC 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: 📚 Git Checkout 15 | uses: actions/checkout@v4 16 | - name: 🎯 Setup Dart 17 | uses: dart-lang/setup-dart@v1 18 | - name: 🐦 Setup Flutter 19 | uses: subosito/flutter-action@v2 20 | - name: 📦 Install Dependencies 21 | run: flutter pub get 22 | - name: 🌵 Dry Run 23 | run: dart pub publish --dry-run 24 | - name: 📢 Publish 25 | run: dart pub publish --force 26 | -------------------------------------------------------------------------------- /.github/workflows/sync_labels.yaml: -------------------------------------------------------------------------------- 1 | name: ♻️ Sync Labels 2 | 3 | on: 4 | push: 5 | paths: 6 | - .github/labels.yml 7 | branches: 8 | - main 9 | workflow_dispatch: 10 | 11 | jobs: 12 | labels: 13 | name: ♻️ Sync labels 14 | runs-on: ubuntu-20.04 15 | steps: 16 | - name: ⤵️ Check out code from GitHub 17 | uses: actions/checkout@v4 18 | 19 | - name: 🚀 Run Label Sync 20 | uses: srealmoreno/label-sync-action@v2 21 | with: 22 | config-file: https://raw.githubusercontent.com/VeryGoodOpenSource/.github/main/.github/labels.yml 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .classpath 21 | .project 22 | .settings/ 23 | .vscode/ 24 | 25 | # Flutter repo-specific 26 | /bin/cache/ 27 | /bin/mingit/ 28 | /dev/benchmarks/mega_gallery/ 29 | /dev/bots/.recipe_deps 30 | /dev/bots/android_tools/ 31 | /dev/docs/doc/ 32 | /dev/docs/flutter.docs.zip 33 | /dev/docs/lib/ 34 | /dev/docs/pubspec.yaml 35 | /dev/integration_tests/**/xcuserdata 36 | /dev/integration_tests/**/Pods 37 | /packages/flutter/coverage/ 38 | version 39 | 40 | # packages file containing multi-root paths 41 | .packages.generated 42 | 43 | # Flutter/Dart/Pub related 44 | **/doc/api/ 45 | .dart_tool/ 46 | .flutter-plugins 47 | .flutter-plugins-dependencies 48 | .packages 49 | .pub-cache/ 50 | .pub/ 51 | build/ 52 | flutter_*.png 53 | linked_*.ds 54 | unlinked.ds 55 | unlinked_spec.ds 56 | 57 | # Android related 58 | **/android/**/gradle-wrapper.jar 59 | **/android/.gradle 60 | **/android/captures/ 61 | **/android/gradlew 62 | **/android/gradlew.bat 63 | **/android/local.properties 64 | **/android/**/GeneratedPluginRegistrant.java 65 | **/android/key.properties 66 | *.jks 67 | 68 | # iOS/XCode related 69 | **/ios/**/*.mode1v3 70 | **/ios/**/*.mode2v3 71 | **/ios/**/*.moved-aside 72 | **/ios/**/*.pbxuser 73 | **/ios/**/*.perspectivev3 74 | **/ios/**/*sync/ 75 | **/ios/**/.sconsign.dblite 76 | **/ios/**/.tags* 77 | **/ios/**/.vagrant/ 78 | **/ios/**/DerivedData/ 79 | **/ios/**/Icon? 80 | **/ios/**/Pods/ 81 | **/ios/**/.symlinks/ 82 | **/ios/**/profile 83 | **/ios/**/xcuserdata 84 | **/ios/.generated/ 85 | **/ios/Flutter/App.framework 86 | **/ios/Flutter/Flutter.framework 87 | **/ios/Flutter/Flutter.podspec 88 | **/ios/Flutter/Generated.xcconfig 89 | **/ios/Flutter/app.flx 90 | **/ios/Flutter/app.zip 91 | **/ios/Flutter/flutter_assets/ 92 | **/ios/Flutter/flutter_export_environment.sh 93 | **/ios/ServiceDefinitions.json 94 | **/ios/Runner/GeneratedPluginRegistrant.* 95 | 96 | # Coverage 97 | coverage/ 98 | .test_coverage.dart 99 | *.lcov 100 | nohup.out 101 | 102 | # Exceptions to above rules. 103 | !**/ios/**/default.mode1v3 104 | !**/ios/**/default.mode2v3 105 | !**/ios/**/default.pbxuser 106 | !**/ios/**/default.perspectivev3 107 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 108 | !/dev/ci/**/Gemfile.lock -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: adc687823a831bbebe28bdccfac1a628ca621513 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.0.0 2 | 3 | **Note**: This release supports [Flutter 3.32.0](https://docs.flutter.dev/release/release-notes/release-notes-3.32.0), for migration details see the [release notes](https://github.com/VeryGoodOpenSource/mockingjay/releases/tag/v2.0.0). 4 | 5 | - chore!: upgrade to flutter 3.32.0 ([#98](https://github.com/VeryGoodOpenSource/mockingjay/pull/98)) 6 | - fix: update constraint in pubspec.yaml ([#101](https://github.com/VeryGoodOpenSource/mockingjay/pull/101)) 7 | 8 | # 1.0.0 9 | 10 | **Note**: This release supports [Flutter 3.29.0](https://docs.flutter.dev/release/release-notes/release-notes-3.29.0). Contains a breaking change, where the `isRoute` `named` parameter has reached the full deprecation cycle. 11 | 12 | - chore(deps): bump very_good_analysis from 6.0.0 to 7.0.0 in /example ([#82](https://github.com/VeryGoodOpenSource/mockingjay/pull/82)) 13 | - chore!: remove deprecated parameter ([#86](https://github.com/VeryGoodOpenSource/mockingjay/pull/86)) 14 | - chore: bumping very good analysis from 6.0.0 to 7.0.0 ([#87](https://github.com/VeryGoodOpenSource/mockingjay/pull/87)) 15 | - feat: support `removeRouteBelow` method ([#90](https://github.com/VeryGoodOpenSource/mockingjay/pull/90)) 16 | - chore: bumping flutter version to 3.29.0 and bumping example dependencies ([#88](https://github.com/VeryGoodOpenSource/mockingjay/pull/88)) 17 | - fix: fixing formatting step ([#92](https://github.com/VeryGoodOpenSource/mockingjay/pull/92)) 18 | 19 | # 0.6.0 20 | 21 | - chore: sync min Dart SDK with min Flutter SDK ([#70](https://github.com/VeryGoodOpenSource/mockingjay/pull/70)) 22 | - chore: tighten dependencies ([#74](https://github.com/VeryGoodOpenSource/mockingjay/pull/74)) 23 | 24 | # 0.5.0 25 | 26 | **Note**: This release supports [Flutter 3.16.0](https://docs.flutter.dev/release/release-notes/release-notes-3.16.0), for migration details see the [release notes](https://github.com/VeryGoodOpenSource/mockingjay/releases/tag/v0.5.0). 27 | 28 | - chore(deps): bump very_good_analysis from 4.0.0+1 to 5.1.0 ([#56](https://github.com/VeryGoodOpenSource/mockingjay/pull/56)) 29 | - chore: update Very Good Analysis to 5.1.0 ([#57](https://github.com/VeryGoodOpenSource/mockingjay/pull/57)) 30 | - chore: updated sdk constraints for example ([#60](https://github.com/VeryGoodOpenSource/mockingjay/pull/60)) 31 | - docs: update installation instructions readme ([#61](https://github.com/VeryGoodOpenSource/mockingjay/pull/61)) 32 | - fix!: Updates for Flutter 3.16.0/Dart 3.2 ([#65](https://github.com/VeryGoodOpenSource/mockingjay/pull/65)) 33 | 34 | # 0.4.0 35 | 36 | - feat: support for `mocktail ^1.0.0` 37 | - feat: support for `flutter 3.13.0` 38 | - feat: sdk constraint to include Dart 3 39 | 40 | # 0.3.0 41 | 42 | - feat: support for `mocktail ^0.3.0` 43 | 44 | # 0.2.0 45 | 46 | - feat: add mock call for `canPop` (thanks [@allisonryan0002](https://github.com/allisonryan0002)!) 47 | - feat: add mock call for `maybePop` (thanks [@korzonkiee](https://github.com/korzonkiee)!) 48 | - feat: add `whereSettings`, `whereName`, `whereArguments`, `whereMaintainState` and `whereFullscreenDialog` matcher arguments to `isRoute` matcher 49 | - **DEPRECATE**: fix: `named` argument on `isRoute` deprecated in favor of `whereName` 50 | 51 | # 0.2.0-dev.3 52 | 53 | - feat: add mock call for `canPop` (thanks [@allisonryan0002](https://github.com/allisonryan0002)!) 54 | 55 | # 0.2.0-dev.2 56 | 57 | - feat: add mock call for `maybePop` (thanks [@korzonkiee](https://github.com/korzonkiee)!) 58 | 59 | # 0.2.0-dev.1 60 | 61 | - feat: add `whereSettings`, `whereName`, `whereArguments`, `whereMaintainState` and `whereFullscreenDialog` matcher arguments to `isRoute` matcher 62 | - **DEPRECATE**: fix: `named` argument on `isRoute` deprecated in favor of `whereName` 63 | 64 | # 0.1.1 65 | 66 | - refactor: reorder mock navigator provider parameters ([#14](https://github.com/VeryGoodOpenSource/mockingjay/pull/14), thanks [@JigneshPatel23](https://github.com/JigneshPatel23)) 67 | 68 | # 0.1.0 69 | 70 | - feat: initial release 🎉 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Very Good Ventures 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🕊 mockingjay 2 | 3 | [![Very Good Ventures][logo_white]][very_good_ventures_link_dark] 4 | [![Very Good Ventures][logo_black]][very_good_ventures_link_light] 5 | 6 | Developed with 💙 by [Very Good Ventures][very_good_ventures_link] 🦄 7 | 8 | [![ci][ci_badge]][ci_link] 9 | [![coverage][coverage_badge]][ci_link] 10 | [![pub package][pub_badge]][pub_link] 11 | [![License: MIT][license_badge]][license_link] 12 | [![style: very good analysis][very_good_analysis_badge]][very_good_analysis_badge_link] 13 | 14 | --- 15 | 16 | A package that makes it easy to mock, test and verify navigation calls in Flutter. It works in tandem with [`mocktail`][mocktail], allowing you to mock a navigator the same way you would any other object, making it easier to test navigation behavior independently from the UI it's supposed to render. 17 | 18 | ## Usage 19 | 20 | To use the package in your tests, install it via `dart pub add`: 21 | 22 | ```shell 23 | dart pub add dev:mockingjay 24 | ``` 25 | 26 | Then, in your tests, create a `MockNavigator` class like so: 27 | 28 | ```dart 29 | import 'package:mockingjay/mockingjay.dart'; 30 | 31 | final navigator = MockNavigator(); 32 | ``` 33 | 34 | Now you can create a new `MockNavigator` and pass it to a `MockNavigatorProvider`. 35 | 36 | Any widget looking up the nearest `Navigator.of(context)` from that point will now receive the `MockNavigator`, allowing you to mock (using `when`) and `verify` any navigation calls. Use the included matchers to more easily match specific route names and types. 37 | 38 | **Note**: make sure the `MockNavigatorProvider` is constructed **below** the `MaterialApp`. Otherwise, any `Navigator.of(context)` call will return a real `NavigatorState` instead of the mock. 39 | 40 | ## Example 41 | 42 | ```dart 43 | import 'package:flutter/material.dart'; 44 | import 'package:flutter_test/flutter_test.dart'; 45 | import 'package:mockingjay/mockingjay.dart'; 46 | 47 | class MyHomePage extends StatelessWidget { 48 | const MyHomePage({super.key}); 49 | 50 | @override 51 | Widget build(BuildContext context) { 52 | return Scaffold( 53 | body: TextButton( 54 | onPressed: () => Navigator.of(context).push(MySettingsPage.route()), 55 | child: const Text('Navigate'), 56 | ), 57 | ); 58 | } 59 | } 60 | 61 | class MySettingsPage extends StatelessWidget { 62 | const MySettingsPage({super.key}); 63 | 64 | static Route route() { 65 | return MaterialPageRoute( 66 | builder: (_) => const MySettingsPage(), 67 | settings: const RouteSettings(name: '/settings'), 68 | ); 69 | } 70 | 71 | @override 72 | Widget build(BuildContext context) { 73 | return const Scaffold(); 74 | } 75 | } 76 | 77 | void main() { 78 | testWidgets('pushes SettingsPage when TextButton is tapped', (tester) async { 79 | final navigator = MockNavigator(); 80 | when(navigator.canPop).thenReturn(true); 81 | when(() => navigator.push(any())).thenAnswer((_) async {}); 82 | 83 | await tester.pumpWidget( 84 | MaterialApp( 85 | home: MockNavigatorProvider( 86 | navigator: navigator, 87 | child: const MyHomePage(), 88 | ), 89 | ), 90 | ); 91 | 92 | await tester.tap(find.byType(TextButton)); 93 | 94 | verify( 95 | () => navigator.push( 96 | any( 97 | that: isRoute( 98 | whereName: equals('/settings'), 99 | ), 100 | ), 101 | ), 102 | ).called(1); 103 | }); 104 | } 105 | 106 | ``` 107 | 108 | [ci_badge]: https://github.com/VeryGoodOpenSource/mockingjay/workflows/mockingjay/badge.svg 109 | [ci_link]: https://github.com/VeryGoodOpenSource/mockingjay/actions 110 | [coverage_badge]: https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/main/coverage_badge.svg 111 | [license_badge]: https://img.shields.io/badge/license-MIT-blue.svg 112 | [license_link]: https://opensource.org/licenses/MIT 113 | [logo_black]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_black.png#gh-light-mode-only 114 | [logo_white]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_white.png#gh-dark-mode-only 115 | [mocktail]: https://pub.dev/packages/mocktail 116 | [pub_badge]: https://img.shields.io/pub/v/mockingjay.svg 117 | [pub_link]: https://pub.dartlang.org/packages/mockingjay 118 | [very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg 119 | [very_good_analysis_badge_link]: https://pub.dev/packages/very_good_analysis 120 | [very_good_ventures_link]: https://verygood.ventures 121 | [very_good_ventures_link_dark]: https://verygood.ventures#gh-dark-mode-only 122 | [very_good_ventures_link_light]: https://verygood.ventures#gh-light-mode-only 123 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:very_good_analysis/analysis_options.yaml 2 | -------------------------------------------------------------------------------- /assets/vgv_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/assets/vgv_logo.png -------------------------------------------------------------------------------- /coverage_badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | coverage 16 | coverage 17 | 100% 18 | 100% 19 | 20 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 02c026b03cd31dd3f867e5faeb7e104cce174c5f 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:very_good_analysis/analysis_options.yaml 2 | linter: 3 | rules: 4 | public_member_api_docs: false 5 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 30 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | defaultConfig { 36 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 37 | applicationId "ventures.verygood.example" 38 | minSdkVersion 16 39 | targetSdkVersion 30 40 | versionCode flutterVersionCode.toInteger() 41 | versionName flutterVersionName 42 | } 43 | 44 | buildTypes { 45 | release { 46 | // TODO: Add your own signing config for the release build. 47 | // Signing with the debug keys for now, so `flutter run --release` works. 48 | signingConfig signingConfigs.debug 49 | } 50 | } 51 | } 52 | 53 | flutter { 54 | source '../..' 55 | } 56 | 57 | dependencies { 58 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 59 | } 60 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 13 | 17 | 21 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/ventures/verygood/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package ventures.verygood.example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.1.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip 7 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/ephemeral/ 22 | Flutter/app.flx 23 | Flutter/app.zip 24 | Flutter/flutter_assets/ 25 | Flutter/flutter_export_environment.sh 26 | ServiceDefinitions.json 27 | Runner/GeneratedPluginRegistrant.* 28 | 29 | # Exceptions to above rules. 30 | !default.mode1v3 31 | !default.mode2v3 32 | !default.pbxuser 33 | !default.perspectivev3 34 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 13 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 14 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 15 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXCopyFilesBuildPhase section */ 19 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 20 | isa = PBXCopyFilesBuildPhase; 21 | buildActionMask = 2147483647; 22 | dstPath = ""; 23 | dstSubfolderSpec = 10; 24 | files = ( 25 | ); 26 | name = "Embed Frameworks"; 27 | runOnlyForDeploymentPostprocessing = 0; 28 | }; 29 | /* End PBXCopyFilesBuildPhase section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 33 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 34 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 35 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 36 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 37 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 38 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 39 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 40 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 42 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 43 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 44 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 45 | /* End PBXFileReference section */ 46 | 47 | /* Begin PBXFrameworksBuildPhase section */ 48 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 49 | isa = PBXFrameworksBuildPhase; 50 | buildActionMask = 2147483647; 51 | files = ( 52 | ); 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | /* End PBXFrameworksBuildPhase section */ 56 | 57 | /* Begin PBXGroup section */ 58 | 9740EEB11CF90186004384FC /* Flutter */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 62 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 63 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 64 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 65 | ); 66 | name = Flutter; 67 | sourceTree = ""; 68 | }; 69 | 97C146E51CF9000F007C117D = { 70 | isa = PBXGroup; 71 | children = ( 72 | 9740EEB11CF90186004384FC /* Flutter */, 73 | 97C146F01CF9000F007C117D /* Runner */, 74 | 97C146EF1CF9000F007C117D /* Products */, 75 | ); 76 | sourceTree = ""; 77 | }; 78 | 97C146EF1CF9000F007C117D /* Products */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 97C146EE1CF9000F007C117D /* Runner.app */, 82 | ); 83 | name = Products; 84 | sourceTree = ""; 85 | }; 86 | 97C146F01CF9000F007C117D /* Runner */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 90 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 91 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 92 | 97C147021CF9000F007C117D /* Info.plist */, 93 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 94 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 95 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 96 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 97 | ); 98 | path = Runner; 99 | sourceTree = ""; 100 | }; 101 | /* End PBXGroup section */ 102 | 103 | /* Begin PBXNativeTarget section */ 104 | 97C146ED1CF9000F007C117D /* Runner */ = { 105 | isa = PBXNativeTarget; 106 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 107 | buildPhases = ( 108 | 9740EEB61CF901F6004384FC /* Run Script */, 109 | 97C146EA1CF9000F007C117D /* Sources */, 110 | 97C146EB1CF9000F007C117D /* Frameworks */, 111 | 97C146EC1CF9000F007C117D /* Resources */, 112 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 113 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 114 | ); 115 | buildRules = ( 116 | ); 117 | dependencies = ( 118 | ); 119 | name = Runner; 120 | productName = Runner; 121 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 122 | productType = "com.apple.product-type.application"; 123 | }; 124 | /* End PBXNativeTarget section */ 125 | 126 | /* Begin PBXProject section */ 127 | 97C146E61CF9000F007C117D /* Project object */ = { 128 | isa = PBXProject; 129 | attributes = { 130 | LastUpgradeCheck = 1020; 131 | ORGANIZATIONNAME = ""; 132 | TargetAttributes = { 133 | 97C146ED1CF9000F007C117D = { 134 | CreatedOnToolsVersion = 7.3.1; 135 | LastSwiftMigration = 1100; 136 | }; 137 | }; 138 | }; 139 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 140 | compatibilityVersion = "Xcode 9.3"; 141 | developmentRegion = en; 142 | hasScannedForEncodings = 0; 143 | knownRegions = ( 144 | en, 145 | Base, 146 | ); 147 | mainGroup = 97C146E51CF9000F007C117D; 148 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 149 | projectDirPath = ""; 150 | projectRoot = ""; 151 | targets = ( 152 | 97C146ED1CF9000F007C117D /* Runner */, 153 | ); 154 | }; 155 | /* End PBXProject section */ 156 | 157 | /* Begin PBXResourcesBuildPhase section */ 158 | 97C146EC1CF9000F007C117D /* Resources */ = { 159 | isa = PBXResourcesBuildPhase; 160 | buildActionMask = 2147483647; 161 | files = ( 162 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 163 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 164 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 165 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 166 | ); 167 | runOnlyForDeploymentPostprocessing = 0; 168 | }; 169 | /* End PBXResourcesBuildPhase section */ 170 | 171 | /* Begin PBXShellScriptBuildPhase section */ 172 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 173 | isa = PBXShellScriptBuildPhase; 174 | buildActionMask = 2147483647; 175 | files = ( 176 | ); 177 | inputPaths = ( 178 | ); 179 | name = "Thin Binary"; 180 | outputPaths = ( 181 | ); 182 | runOnlyForDeploymentPostprocessing = 0; 183 | shellPath = /bin/sh; 184 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 185 | }; 186 | 9740EEB61CF901F6004384FC /* Run Script */ = { 187 | isa = PBXShellScriptBuildPhase; 188 | buildActionMask = 2147483647; 189 | files = ( 190 | ); 191 | inputPaths = ( 192 | ); 193 | name = "Run Script"; 194 | outputPaths = ( 195 | ); 196 | runOnlyForDeploymentPostprocessing = 0; 197 | shellPath = /bin/sh; 198 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 199 | }; 200 | /* End PBXShellScriptBuildPhase section */ 201 | 202 | /* Begin PBXSourcesBuildPhase section */ 203 | 97C146EA1CF9000F007C117D /* Sources */ = { 204 | isa = PBXSourcesBuildPhase; 205 | buildActionMask = 2147483647; 206 | files = ( 207 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 208 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 209 | ); 210 | runOnlyForDeploymentPostprocessing = 0; 211 | }; 212 | /* End PBXSourcesBuildPhase section */ 213 | 214 | /* Begin PBXVariantGroup section */ 215 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 216 | isa = PBXVariantGroup; 217 | children = ( 218 | 97C146FB1CF9000F007C117D /* Base */, 219 | ); 220 | name = Main.storyboard; 221 | sourceTree = ""; 222 | }; 223 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 224 | isa = PBXVariantGroup; 225 | children = ( 226 | 97C147001CF9000F007C117D /* Base */, 227 | ); 228 | name = LaunchScreen.storyboard; 229 | sourceTree = ""; 230 | }; 231 | /* End PBXVariantGroup section */ 232 | 233 | /* Begin XCBuildConfiguration section */ 234 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 235 | isa = XCBuildConfiguration; 236 | buildSettings = { 237 | ALWAYS_SEARCH_USER_PATHS = NO; 238 | CLANG_ANALYZER_NONNULL = YES; 239 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 240 | CLANG_CXX_LIBRARY = "libc++"; 241 | CLANG_ENABLE_MODULES = YES; 242 | CLANG_ENABLE_OBJC_ARC = YES; 243 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 244 | CLANG_WARN_BOOL_CONVERSION = YES; 245 | CLANG_WARN_COMMA = YES; 246 | CLANG_WARN_CONSTANT_CONVERSION = YES; 247 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 248 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 249 | CLANG_WARN_EMPTY_BODY = YES; 250 | CLANG_WARN_ENUM_CONVERSION = YES; 251 | CLANG_WARN_INFINITE_RECURSION = YES; 252 | CLANG_WARN_INT_CONVERSION = YES; 253 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 254 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 255 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 256 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 257 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 258 | CLANG_WARN_STRICT_PROTOTYPES = YES; 259 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 260 | CLANG_WARN_UNREACHABLE_CODE = YES; 261 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 262 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 263 | COPY_PHASE_STRIP = NO; 264 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 265 | ENABLE_NS_ASSERTIONS = NO; 266 | ENABLE_STRICT_OBJC_MSGSEND = YES; 267 | GCC_C_LANGUAGE_STANDARD = gnu99; 268 | GCC_NO_COMMON_BLOCKS = YES; 269 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 270 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 271 | GCC_WARN_UNDECLARED_SELECTOR = YES; 272 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 273 | GCC_WARN_UNUSED_FUNCTION = YES; 274 | GCC_WARN_UNUSED_VARIABLE = YES; 275 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 276 | MTL_ENABLE_DEBUG_INFO = NO; 277 | SDKROOT = iphoneos; 278 | SUPPORTED_PLATFORMS = iphoneos; 279 | TARGETED_DEVICE_FAMILY = "1,2"; 280 | VALIDATE_PRODUCT = YES; 281 | }; 282 | name = Profile; 283 | }; 284 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 285 | isa = XCBuildConfiguration; 286 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 287 | buildSettings = { 288 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 289 | CLANG_ENABLE_MODULES = YES; 290 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 291 | ENABLE_BITCODE = NO; 292 | INFOPLIST_FILE = Runner/Info.plist; 293 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 294 | PRODUCT_BUNDLE_IDENTIFIER = ventures.verygood.example; 295 | PRODUCT_NAME = "$(TARGET_NAME)"; 296 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 297 | SWIFT_VERSION = 5.0; 298 | VERSIONING_SYSTEM = "apple-generic"; 299 | }; 300 | name = Profile; 301 | }; 302 | 97C147031CF9000F007C117D /* Debug */ = { 303 | isa = XCBuildConfiguration; 304 | buildSettings = { 305 | ALWAYS_SEARCH_USER_PATHS = NO; 306 | CLANG_ANALYZER_NONNULL = YES; 307 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 308 | CLANG_CXX_LIBRARY = "libc++"; 309 | CLANG_ENABLE_MODULES = YES; 310 | CLANG_ENABLE_OBJC_ARC = YES; 311 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 312 | CLANG_WARN_BOOL_CONVERSION = YES; 313 | CLANG_WARN_COMMA = YES; 314 | CLANG_WARN_CONSTANT_CONVERSION = YES; 315 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 316 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 317 | CLANG_WARN_EMPTY_BODY = YES; 318 | CLANG_WARN_ENUM_CONVERSION = YES; 319 | CLANG_WARN_INFINITE_RECURSION = YES; 320 | CLANG_WARN_INT_CONVERSION = YES; 321 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 322 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 323 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 324 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 325 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 326 | CLANG_WARN_STRICT_PROTOTYPES = YES; 327 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 328 | CLANG_WARN_UNREACHABLE_CODE = YES; 329 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 330 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 331 | COPY_PHASE_STRIP = NO; 332 | DEBUG_INFORMATION_FORMAT = dwarf; 333 | ENABLE_STRICT_OBJC_MSGSEND = YES; 334 | ENABLE_TESTABILITY = YES; 335 | GCC_C_LANGUAGE_STANDARD = gnu99; 336 | GCC_DYNAMIC_NO_PIC = NO; 337 | GCC_NO_COMMON_BLOCKS = YES; 338 | GCC_OPTIMIZATION_LEVEL = 0; 339 | GCC_PREPROCESSOR_DEFINITIONS = ( 340 | "DEBUG=1", 341 | "$(inherited)", 342 | ); 343 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 344 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 345 | GCC_WARN_UNDECLARED_SELECTOR = YES; 346 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 347 | GCC_WARN_UNUSED_FUNCTION = YES; 348 | GCC_WARN_UNUSED_VARIABLE = YES; 349 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 350 | MTL_ENABLE_DEBUG_INFO = YES; 351 | ONLY_ACTIVE_ARCH = YES; 352 | SDKROOT = iphoneos; 353 | TARGETED_DEVICE_FAMILY = "1,2"; 354 | }; 355 | name = Debug; 356 | }; 357 | 97C147041CF9000F007C117D /* Release */ = { 358 | isa = XCBuildConfiguration; 359 | buildSettings = { 360 | ALWAYS_SEARCH_USER_PATHS = NO; 361 | CLANG_ANALYZER_NONNULL = YES; 362 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 363 | CLANG_CXX_LIBRARY = "libc++"; 364 | CLANG_ENABLE_MODULES = YES; 365 | CLANG_ENABLE_OBJC_ARC = YES; 366 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 367 | CLANG_WARN_BOOL_CONVERSION = YES; 368 | CLANG_WARN_COMMA = YES; 369 | CLANG_WARN_CONSTANT_CONVERSION = YES; 370 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 371 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 372 | CLANG_WARN_EMPTY_BODY = YES; 373 | CLANG_WARN_ENUM_CONVERSION = YES; 374 | CLANG_WARN_INFINITE_RECURSION = YES; 375 | CLANG_WARN_INT_CONVERSION = YES; 376 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 377 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 378 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 379 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 380 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 381 | CLANG_WARN_STRICT_PROTOTYPES = YES; 382 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 383 | CLANG_WARN_UNREACHABLE_CODE = YES; 384 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 385 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 386 | COPY_PHASE_STRIP = NO; 387 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 388 | ENABLE_NS_ASSERTIONS = NO; 389 | ENABLE_STRICT_OBJC_MSGSEND = YES; 390 | GCC_C_LANGUAGE_STANDARD = gnu99; 391 | GCC_NO_COMMON_BLOCKS = YES; 392 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 393 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 394 | GCC_WARN_UNDECLARED_SELECTOR = YES; 395 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 396 | GCC_WARN_UNUSED_FUNCTION = YES; 397 | GCC_WARN_UNUSED_VARIABLE = YES; 398 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 399 | MTL_ENABLE_DEBUG_INFO = NO; 400 | SDKROOT = iphoneos; 401 | SUPPORTED_PLATFORMS = iphoneos; 402 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 403 | TARGETED_DEVICE_FAMILY = "1,2"; 404 | VALIDATE_PRODUCT = YES; 405 | }; 406 | name = Release; 407 | }; 408 | 97C147061CF9000F007C117D /* Debug */ = { 409 | isa = XCBuildConfiguration; 410 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 411 | buildSettings = { 412 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 413 | CLANG_ENABLE_MODULES = YES; 414 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 415 | ENABLE_BITCODE = NO; 416 | INFOPLIST_FILE = Runner/Info.plist; 417 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 418 | PRODUCT_BUNDLE_IDENTIFIER = ventures.verygood.example; 419 | PRODUCT_NAME = "$(TARGET_NAME)"; 420 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 421 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 422 | SWIFT_VERSION = 5.0; 423 | VERSIONING_SYSTEM = "apple-generic"; 424 | }; 425 | name = Debug; 426 | }; 427 | 97C147071CF9000F007C117D /* Release */ = { 428 | isa = XCBuildConfiguration; 429 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 430 | buildSettings = { 431 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 432 | CLANG_ENABLE_MODULES = YES; 433 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 434 | ENABLE_BITCODE = NO; 435 | INFOPLIST_FILE = Runner/Info.plist; 436 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 437 | PRODUCT_BUNDLE_IDENTIFIER = ventures.verygood.example; 438 | PRODUCT_NAME = "$(TARGET_NAME)"; 439 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 440 | SWIFT_VERSION = 5.0; 441 | VERSIONING_SYSTEM = "apple-generic"; 442 | }; 443 | name = Release; 444 | }; 445 | /* End XCBuildConfiguration section */ 446 | 447 | /* Begin XCConfigurationList section */ 448 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 449 | isa = XCConfigurationList; 450 | buildConfigurations = ( 451 | 97C147031CF9000F007C117D /* Debug */, 452 | 97C147041CF9000F007C117D /* Release */, 453 | 249021D3217E4FDB00AE95B9 /* Profile */, 454 | ); 455 | defaultConfigurationIsVisible = 0; 456 | defaultConfigurationName = Release; 457 | }; 458 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 459 | isa = XCConfigurationList; 460 | buildConfigurations = ( 461 | 97C147061CF9000F007C117D /* Debug */, 462 | 97C147071CF9000F007C117D /* Release */, 463 | 249021D4217E4FDB00AE95B9 /* Profile */, 464 | ); 465 | defaultConfigurationIsVisible = 0; 466 | defaultConfigurationName = Release; 467 | }; 468 | /* End XCConfigurationList section */ 469 | }; 470 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 471 | } 472 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/lib/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/ui/ui.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class App extends StatelessWidget { 5 | const App({super.key}); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return MaterialApp( 10 | title: 'Mockingjay Example', 11 | color: Colors.black, 12 | theme: ThemeData.light().copyWith(primaryColor: Colors.black), 13 | darkTheme: ThemeData.dark().copyWith(primaryColor: Colors.black), 14 | home: const HomeScreen(), 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/app.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | void main() => runApp(const App()); 5 | -------------------------------------------------------------------------------- /example/lib/ui/home_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/ui/ui.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:universal_io/io.dart'; 4 | 5 | class HomeScreen extends StatefulWidget { 6 | const HomeScreen({super.key}); 7 | 8 | @override 9 | State createState() => _HomeScreenState(); 10 | } 11 | 12 | class _HomeScreenState extends State { 13 | Future _showPincodeScreen(BuildContext context) async { 14 | final scaffoldMessenger = ScaffoldMessenger.of(context); 15 | final result = await Navigator.of(context).push(PincodeScreen.route()); 16 | 17 | if (!mounted) { 18 | return; 19 | } 20 | 21 | late final String snackBarContent; 22 | 23 | if (result == null) { 24 | snackBarContent = 'No pincode submitted. 😲'; 25 | } else { 26 | snackBarContent = 'Pincode is "$result" 🔒'; 27 | } 28 | 29 | scaffoldMessenger 30 | ..removeCurrentSnackBar() 31 | ..showSnackBar( 32 | SnackBar( 33 | behavior: SnackBarBehavior.floating, 34 | content: Text(snackBarContent), 35 | ), 36 | ); 37 | } 38 | 39 | Future _showQuizDialog(BuildContext context) async { 40 | final scaffoldMessenger = ScaffoldMessenger.of(context); 41 | final result = await QuizDialog.show(context); 42 | 43 | if (!mounted) { 44 | return; 45 | } 46 | 47 | late final String snackBarContent; 48 | 49 | if (result == null) { 50 | snackBarContent = 'No answer selected. 😲'; 51 | } else if (result == QuizOption.pizza) { 52 | snackBarContent = 'Pizza all the way! 🍕'; 53 | } else if (result == QuizOption.hamburger) { 54 | snackBarContent = 'Hamburger all the way! 🍔'; 55 | } 56 | 57 | scaffoldMessenger 58 | ..removeCurrentSnackBar() 59 | ..showSnackBar( 60 | SnackBar( 61 | behavior: SnackBarBehavior.floating, 62 | content: Text(snackBarContent), 63 | ), 64 | ); 65 | } 66 | 67 | @override 68 | Widget build(BuildContext context) { 69 | final theme = Theme.of(context); 70 | final monospaceFontFamily = Platform.isIOS ? 'Courier' : 'monospace'; 71 | 72 | return Scaffold( 73 | appBar: AppBar(title: const Text('Mockingjay Example')), 74 | body: Center( 75 | child: DefaultTextStyle.merge( 76 | textAlign: TextAlign.center, 77 | child: SingleChildScrollView( 78 | child: Padding( 79 | padding: const EdgeInsets.all(32), 80 | child: Column( 81 | mainAxisAlignment: MainAxisAlignment.center, 82 | children: [ 83 | Text( 84 | 'This is an example app showcasing the Mockingjay library.', 85 | style: theme.textTheme.titleLarge, 86 | ), 87 | const SizedBox(height: 8), 88 | Text.rich( 89 | TextSpan( 90 | children: [ 91 | const TextSpan( 92 | text: 93 | 'Use one of the following buttons to trigger a ' 94 | 'navigation change.\n' 95 | 'Check out the test files in the ', 96 | ), 97 | TextSpan( 98 | text: 'test/', 99 | style: TextStyle( 100 | fontFamily: monospaceFontFamily, 101 | fontWeight: FontWeight.bold, 102 | ), 103 | ), 104 | const TextSpan( 105 | text: ' directory to see how the library works.', 106 | ), 107 | ], 108 | ), 109 | style: TextStyle(color: theme.disabledColor), 110 | ), 111 | const SizedBox(height: 32), 112 | TextButton.icon( 113 | key: const Key('homeScreen_showPincodeScreen_textButton'), 114 | onPressed: () => _showPincodeScreen(context), 115 | label: const Text('Show Pincode Screen'), 116 | icon: const Icon(Icons.chevron_right_rounded), 117 | ), 118 | TextButton.icon( 119 | key: const Key('homeScreen_showQuizDialog_textButton'), 120 | onPressed: () => _showQuizDialog(context), 121 | label: const Text('Show Quiz Dialog'), 122 | icon: const Icon(Icons.view_array), 123 | ), 124 | ], 125 | ), 126 | ), 127 | ), 128 | ), 129 | ), 130 | ); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /example/lib/ui/pincode_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:universal_io/io.dart'; 4 | 5 | class PincodeScreen extends StatefulWidget { 6 | const PincodeScreen({super.key}); 7 | 8 | static Route route() { 9 | return MaterialPageRoute( 10 | settings: const RouteSettings(name: '/pincode_screen'), 11 | fullscreenDialog: true, 12 | builder: (context) { 13 | return const PincodeScreen(); 14 | }, 15 | ); 16 | } 17 | 18 | @override 19 | State createState() => _PincodeScreenState(); 20 | } 21 | 22 | class _PincodeScreenState extends State { 23 | var _pincode = ''; 24 | String? _errorText; 25 | 26 | void _onPincodeChanged(BuildContext context, String pincode) { 27 | if (!mounted) { 28 | return; 29 | } 30 | 31 | setState(() { 32 | _pincode = pincode; 33 | _errorText = null; 34 | }); 35 | 36 | if (_pincode.length >= 6) { 37 | Navigator.of(context).pop(_pincode); 38 | } 39 | } 40 | 41 | void _onSubmitted() { 42 | if (_pincode.length != 6) { 43 | setState(() { 44 | _errorText = 'Pincode must be 6 digits long'; 45 | }); 46 | } 47 | } 48 | 49 | @override 50 | Widget build(BuildContext context) { 51 | final monospaceFontFamily = Platform.isIOS ? 'Courier' : 'monospace'; 52 | 53 | return Scaffold( 54 | appBar: AppBar(title: const Text('Pincode Screen')), 55 | body: Center( 56 | child: DefaultTextStyle.merge( 57 | textAlign: TextAlign.center, 58 | child: SingleChildScrollView( 59 | child: Padding( 60 | padding: const EdgeInsets.all(32), 61 | child: Column( 62 | mainAxisAlignment: MainAxisAlignment.center, 63 | children: [ 64 | const Text('Enter a pincode to continue.'), 65 | const SizedBox(height: 32), 66 | TextField( 67 | autofocus: true, 68 | minLines: 1, 69 | maxLength: 6, 70 | maxLengthEnforcement: MaxLengthEnforcement.enforced, 71 | obscureText: true, 72 | style: TextStyle( 73 | fontFamily: monospaceFontFamily, 74 | fontSize: 48, 75 | ), 76 | decoration: InputDecoration( 77 | errorText: _errorText, 78 | hintText: '200798', 79 | hintStyle: TextStyle( 80 | color: Theme.of(context).disabledColor, 81 | ), 82 | ), 83 | onChanged: (value) => _onPincodeChanged(context, value), 84 | onSubmitted: (value) => _onSubmitted(), 85 | onEditingComplete: _onSubmitted, 86 | ), 87 | ], 88 | ), 89 | ), 90 | ), 91 | ), 92 | ), 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /example/lib/ui/quiz_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | enum QuizOption { pizza, hamburger } 4 | 5 | class QuizDialog extends StatelessWidget { 6 | const QuizDialog({super.key}); 7 | 8 | static Future show(BuildContext context) { 9 | return showCupertinoDialog( 10 | context: context, 11 | // Important for compatibility with MockNavigator. 12 | useRootNavigator: false, 13 | builder: (context) { 14 | return const QuizDialog(); 15 | }, 16 | ); 17 | } 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return CupertinoAlertDialog( 22 | content: const Text('Which food is the best?'), 23 | actions: [ 24 | CupertinoDialogAction( 25 | onPressed: () => Navigator.of(context).pop(QuizOption.pizza), 26 | child: const Text('🍕'), 27 | ), 28 | CupertinoDialogAction( 29 | onPressed: () => Navigator.of(context).pop(QuizOption.hamburger), 30 | child: const Text('🍔'), 31 | ), 32 | ], 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /example/lib/ui/ui.dart: -------------------------------------------------------------------------------- 1 | export 'home_screen.dart'; 2 | export 'pincode_screen.dart'; 3 | export 'quiz_dialog.dart'; 4 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: An example project showcasing the functionality of Mockingjay. 3 | version: 0.0.1 4 | homepage: https://github.com/VeryGoodOpenSource/mockingjay 5 | 6 | environment: 7 | sdk: ^3.8.0 8 | flutter: ^3.32.0 9 | 10 | dependencies: 11 | bloc: ^9.0.0 12 | equatable: ^2.0.7 13 | flutter: 14 | sdk: flutter 15 | flutter_bloc: ^9.1.1 16 | universal_io: ^2.2.2 17 | 18 | dev_dependencies: 19 | bloc_test: ^10.0.0 20 | flutter_test: 21 | sdk: flutter 22 | mockingjay: 23 | path: ../ 24 | mocktail: ^1.0.4 25 | very_good_analysis: ^8.0.0 26 | 27 | flutter: 28 | uses-material-design: true 29 | -------------------------------------------------------------------------------- /example/test/app_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/app.dart'; 2 | import 'package:example/ui/ui.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | import 'helpers.dart'; 6 | 7 | void main() { 8 | group('App', () { 9 | testWidgets('renders HomeScreen by default', (tester) async { 10 | await tester.pumpTest( 11 | builder: (context) { 12 | return const App(); 13 | }, 14 | ); 15 | 16 | expect(find.byType(HomeScreen), findsOneWidget); 17 | }); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /example/test/helpers.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | extension WidgetTesterX on WidgetTester { 5 | Future pumpTest({required WidgetBuilder builder}) async { 6 | await pumpWidget( 7 | MaterialApp( 8 | title: 'Mock Navigator Test', 9 | home: Scaffold(body: Builder(builder: builder)), 10 | ), 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/test/ui/home_screen_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/ui/ui.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | import 'package:mockingjay/mockingjay.dart'; 6 | 7 | import '../helpers.dart'; 8 | 9 | class FakeRoute extends Fake implements Route {} 10 | 11 | void main() { 12 | group('HomeScreen', () { 13 | const showPincodeScreenTextButtonKey = Key( 14 | 'homeScreen_showPincodeScreen_textButton', 15 | ); 16 | const showQuizDialogTextButtonKey = Key( 17 | 'homeScreen_showQuizDialog_textButton', 18 | ); 19 | 20 | late MockNavigator navigator; 21 | 22 | setUpAll(() { 23 | registerFallbackValue(FakeRoute()); 24 | registerFallbackValue(FakeRoute()); 25 | }); 26 | 27 | setUp(() { 28 | navigator = MockNavigator(); 29 | when(() => navigator.canPop()).thenReturn(true); 30 | when(() => navigator.push(any())).thenAnswer((_) async => null); 31 | when( 32 | () => navigator.push(any()), 33 | ).thenAnswer((_) async => null); 34 | }); 35 | 36 | group('show pincode screen button', () { 37 | testWidgets('is rendered', (tester) async { 38 | await tester.pumpTest( 39 | builder: (context) { 40 | return const HomeScreen(); 41 | }, 42 | ); 43 | 44 | expect(find.byKey(showPincodeScreenTextButtonKey), findsOneWidget); 45 | }); 46 | 47 | testWidgets('navigates to PincodeScreen when pressed', (tester) async { 48 | await tester.pumpTest( 49 | builder: (context) { 50 | return MockNavigatorProvider( 51 | navigator: navigator, 52 | child: const HomeScreen(), 53 | ); 54 | }, 55 | ); 56 | 57 | await tester.tap(find.byKey(showPincodeScreenTextButtonKey)); 58 | 59 | verify( 60 | () => navigator.push( 61 | any(that: isRoute(whereName: equals('/pincode_screen'))), 62 | ), 63 | ).called(1); 64 | }); 65 | 66 | testWidgets('displays snackbar with selected pincode', (tester) async { 67 | when( 68 | () => navigator.push( 69 | any(that: isRoute(whereName: equals('/pincode_screen'))), 70 | ), 71 | ).thenAnswer((_) async => '123456'); 72 | 73 | await tester.pumpTest( 74 | builder: (context) { 75 | return MockNavigatorProvider( 76 | navigator: navigator, 77 | child: const HomeScreen(), 78 | ); 79 | }, 80 | ); 81 | 82 | await tester.tap(find.byKey(showPincodeScreenTextButtonKey)); 83 | await tester.pumpAndSettle(); 84 | 85 | expect( 86 | find.widgetWithText(SnackBar, 'Pincode is "123456" 🔒'), 87 | findsOneWidget, 88 | ); 89 | }); 90 | 91 | testWidgets('displays snackbar when no pincode was submitted', ( 92 | tester, 93 | ) async { 94 | when( 95 | () => navigator.push( 96 | any(that: isRoute(whereName: equals('/pincode_screen'))), 97 | ), 98 | ).thenAnswer((_) async => null); 99 | 100 | await tester.pumpTest( 101 | builder: (context) { 102 | return MockNavigatorProvider( 103 | navigator: navigator, 104 | child: const HomeScreen(), 105 | ); 106 | }, 107 | ); 108 | 109 | await tester.tap(find.byKey(showPincodeScreenTextButtonKey)); 110 | await tester.pumpAndSettle(); 111 | 112 | expect( 113 | find.widgetWithText(SnackBar, 'No pincode submitted. 😲'), 114 | findsOneWidget, 115 | ); 116 | }); 117 | }); 118 | 119 | group('show quiz dialog button', () { 120 | testWidgets('is rendered', (tester) async { 121 | await tester.pumpTest( 122 | builder: (context) { 123 | return const HomeScreen(); 124 | }, 125 | ); 126 | 127 | expect(find.byKey(showQuizDialogTextButtonKey), findsOneWidget); 128 | }); 129 | 130 | testWidgets('shows quiz dialog when pressed', (tester) async { 131 | await tester.pumpTest( 132 | builder: (context) { 133 | return MockNavigatorProvider( 134 | navigator: navigator, 135 | child: const HomeScreen(), 136 | ); 137 | }, 138 | ); 139 | 140 | await tester.tap(find.byKey(showQuizDialogTextButtonKey)); 141 | 142 | verify( 143 | () => navigator.push(any(that: isRoute())), 144 | ).called(1); 145 | }); 146 | 147 | testWidgets('displays snackbar when pizza was selected', (tester) async { 148 | when( 149 | () => navigator.push(any(that: isRoute())), 150 | ).thenAnswer((_) async => QuizOption.pizza); 151 | 152 | await tester.pumpTest( 153 | builder: (context) { 154 | return MockNavigatorProvider( 155 | navigator: navigator, 156 | child: const HomeScreen(), 157 | ); 158 | }, 159 | ); 160 | 161 | await tester.tap(find.byKey(showQuizDialogTextButtonKey)); 162 | await tester.pumpAndSettle(); 163 | 164 | expect( 165 | find.widgetWithText(SnackBar, 'Pizza all the way! 🍕'), 166 | findsOneWidget, 167 | ); 168 | }); 169 | 170 | testWidgets('displays snackbar when hamburger was selected', ( 171 | tester, 172 | ) async { 173 | when( 174 | () => navigator.push(any(that: isRoute())), 175 | ).thenAnswer((_) async => QuizOption.hamburger); 176 | 177 | await tester.pumpTest( 178 | builder: (context) { 179 | return MockNavigatorProvider( 180 | navigator: navigator, 181 | child: const HomeScreen(), 182 | ); 183 | }, 184 | ); 185 | 186 | await tester.tap(find.byKey(showQuizDialogTextButtonKey)); 187 | await tester.pumpAndSettle(); 188 | 189 | expect( 190 | find.widgetWithText(SnackBar, 'Hamburger all the way! 🍔'), 191 | findsOneWidget, 192 | ); 193 | }); 194 | 195 | testWidgets('displays snackbar when no answer was selected', ( 196 | tester, 197 | ) async { 198 | when( 199 | () => navigator.push(any(that: isRoute())), 200 | ).thenAnswer((_) async => null); 201 | 202 | await tester.pumpTest( 203 | builder: (context) { 204 | return MockNavigatorProvider( 205 | navigator: navigator, 206 | child: const HomeScreen(), 207 | ); 208 | }, 209 | ); 210 | 211 | await tester.tap(find.byKey(showQuizDialogTextButtonKey)); 212 | await tester.pumpAndSettle(); 213 | 214 | expect( 215 | find.widgetWithText(SnackBar, 'No answer selected. 😲'), 216 | findsOneWidget, 217 | ); 218 | }); 219 | }); 220 | }); 221 | } 222 | -------------------------------------------------------------------------------- /example/test/ui/pincode_screen_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:example/ui/ui.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | import 'package:mockingjay/mockingjay.dart'; 8 | 9 | import '../helpers.dart'; 10 | 11 | void main() { 12 | group('PincodeScreen', () { 13 | late MockNavigator navigator; 14 | 15 | setUp(() { 16 | navigator = MockNavigator(); 17 | when(() => navigator.canPop()).thenReturn(true); 18 | }); 19 | 20 | testWidgets('.route renders PincodeScreen', (tester) async { 21 | late BuildContext context; 22 | await tester.pumpTest( 23 | builder: (appContext) { 24 | context = appContext; 25 | return const SizedBox(); 26 | }, 27 | ); 28 | unawaited(Navigator.of(context).push(PincodeScreen.route())); 29 | await tester.pumpAndSettle(); 30 | expect(find.byType(PincodeScreen), findsOneWidget); 31 | }); 32 | 33 | testWidgets('renders pincode text input', (tester) async { 34 | await tester.pumpTest( 35 | builder: (context) { 36 | return const PincodeScreen(); 37 | }, 38 | ); 39 | 40 | expect(find.byType(TextField), findsOneWidget); 41 | }); 42 | 43 | testWidgets( 44 | 'pops route with entered pincode when 6 digits have been entered', 45 | (tester) async { 46 | await tester.pumpTest( 47 | builder: (context) { 48 | return MockNavigatorProvider( 49 | navigator: navigator, 50 | child: const PincodeScreen(), 51 | ); 52 | }, 53 | ); 54 | 55 | await tester.enterText(find.byType(TextField), '1234'); 56 | verifyNever(() => navigator.pop('1234')); 57 | 58 | await tester.enterText(find.byType(TextField), '12345'); 59 | verifyNever(() => navigator.pop('12345')); 60 | 61 | await tester.enterText(find.byType(TextField), '123456'); 62 | verify(() => navigator.pop('123456')).called(1); 63 | }, 64 | ); 65 | 66 | testWidgets('clamps to 6 digits when exact 6 digits have been entered', ( 67 | tester, 68 | ) async { 69 | await tester.pumpTest( 70 | builder: (context) { 71 | return MockNavigatorProvider( 72 | navigator: navigator, 73 | child: const PincodeScreen(), 74 | ); 75 | }, 76 | ); 77 | 78 | await tester.enterText(find.byType(TextField), '123456'); 79 | verify(() => navigator.pop('123456')).called(1); 80 | }); 81 | 82 | testWidgets( 83 | 'clamps to 6 digits when more than 6 digits have been entered', 84 | (tester) async { 85 | await tester.pumpTest( 86 | builder: (context) { 87 | return MockNavigatorProvider( 88 | navigator: navigator, 89 | child: const PincodeScreen(), 90 | ); 91 | }, 92 | ); 93 | 94 | await tester.enterText(find.byType(TextField), '1234567'); 95 | verify(() => navigator.pop('123456')).called(1); 96 | }, 97 | ); 98 | 99 | testWidgets('shows error when less than 6 digits have been entered', ( 100 | tester, 101 | ) async { 102 | await tester.pumpTest( 103 | builder: (context) { 104 | return MockNavigatorProvider( 105 | navigator: navigator, 106 | child: const PincodeScreen(), 107 | ); 108 | }, 109 | ); 110 | 111 | await tester.enterText(find.byType(TextField), '12345'); 112 | await tester.testTextInput.receiveAction(TextInputAction.done); 113 | await tester.pump(); 114 | expect(find.text('Pincode must be 6 digits long'), findsOneWidget); 115 | }); 116 | }); 117 | } 118 | -------------------------------------------------------------------------------- /example/test/ui/quiz_dialog_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:example/ui/ui.dart'; 4 | import 'package:flutter/widgets.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | import 'package:mockingjay/mockingjay.dart'; 8 | 9 | import '../helpers.dart'; 10 | 11 | void main() { 12 | group('QuizDialog', () { 13 | late MockNavigator navigator; 14 | 15 | setUp(() { 16 | navigator = MockNavigator(); 17 | when(() => navigator.canPop()).thenReturn(true); 18 | }); 19 | 20 | testWidgets('.show opens dialog', (tester) async { 21 | late BuildContext context; 22 | await tester.pumpTest( 23 | builder: (appContext) { 24 | context = appContext; 25 | return const SizedBox(); 26 | }, 27 | ); 28 | unawaited(QuizDialog.show(context)); 29 | await tester.pumpAndSettle(); 30 | expect(find.byType(QuizDialog), findsOneWidget); 31 | }); 32 | 33 | group('pizza button', () { 34 | testWidgets('is rendered', (tester) async { 35 | await tester.pumpTest( 36 | builder: (context) { 37 | return const QuizDialog(); 38 | }, 39 | ); 40 | 41 | expect(find.text('🍕'), findsOneWidget); 42 | }); 43 | 44 | testWidgets('pops route with pizza option when pressed', (tester) async { 45 | await tester.pumpTest( 46 | builder: (context) { 47 | return MockNavigatorProvider( 48 | navigator: navigator, 49 | child: const QuizDialog(), 50 | ); 51 | }, 52 | ); 53 | 54 | await tester.tap(find.text('🍕')); 55 | 56 | verify(() => navigator.pop(QuizOption.pizza)).called(1); 57 | }); 58 | }); 59 | 60 | group('hamburger button', () { 61 | testWidgets('is rendered', (tester) async { 62 | await tester.pumpTest( 63 | builder: (context) { 64 | return const QuizDialog(); 65 | }, 66 | ); 67 | 68 | expect(find.text('🍔'), findsOneWidget); 69 | }); 70 | 71 | testWidgets('pops route with hamburger option when pressed', ( 72 | tester, 73 | ) async { 74 | await tester.pumpTest( 75 | builder: (context) { 76 | return MockNavigatorProvider( 77 | navigator: navigator, 78 | child: const QuizDialog(), 79 | ); 80 | }, 81 | ); 82 | 83 | await tester.tap(find.text('🍔')); 84 | 85 | verify(() => navigator.pop(QuizOption.hamburger)).called(1); 86 | }); 87 | }); 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/web/favicon.png -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeryGoodOpenSource/mockingjay/17943cdf73847f672df51308d6a90c5db268ad8d/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | example 27 | 28 | 29 | 30 | 33 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /example/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "short_name": "example", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /lib/mockingjay.dart: -------------------------------------------------------------------------------- 1 | /// A package that makes it easy to mock, test and verify 2 | /// navigation calls in Flutter. 3 | library; 4 | 5 | export 'package:mocktail/mocktail.dart'; 6 | 7 | export 'src/matchers.dart'; 8 | export 'src/mock_navigator.dart'; 9 | -------------------------------------------------------------------------------- /lib/src/matcher_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:matcher/matcher.dart'; 2 | 3 | /// Extensions on [Matcher] for convenience. 4 | extension MatcherExtensions on Matcher { 5 | /// Returns the description of this matcher as a string. 6 | String describeAsString() { 7 | return describe(StringDescription()).toString(); 8 | } 9 | 10 | /// Returns the mismatch description of this matcher as a string. 11 | // ignore: avoid_positional_boolean_parameters 12 | String describeMismatchAsString( 13 | dynamic item, 14 | Map matchState, { 15 | required bool verbose, 16 | }) { 17 | final description = describeMismatch( 18 | item, 19 | StringDescription(), 20 | matchState, 21 | verbose, 22 | ); 23 | 24 | if (description.toString().isEmpty) { 25 | return 'is $item instead of ${describeAsString()}'; 26 | } else { 27 | return description.toString(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/matchers.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:mockingjay/src/matcher_extensions.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | /// Returns a matcher that matches [Route]s. 6 | /// 7 | /// The optional type [T] is the return type of the route. In most cases, this 8 | /// will be `void`, and can be omitted. 9 | /// 10 | /// Additional matchers can be provided, such as [whereSettings], [whereName], 11 | /// [whereArguments], [whereMaintainState] and [whereFullscreenDialog]. 12 | /// ```dart 13 | /// expect(fooRoute, isRoute(whereName: equals('/home'))); 14 | /// 15 | /// verify( 16 | /// () => navigator.push(any(that: isRoute(whereName: equals('/home')))), 17 | /// ).called(1); 18 | /// 19 | /// ``` 20 | Matcher isRoute({ 21 | Matcher? whereSettings, 22 | Matcher? whereName, 23 | Matcher? whereArguments, 24 | Matcher? whereMaintainState, 25 | Matcher? whereFullscreenDialog, 26 | }) { 27 | assert( 28 | whereSettings == null || (whereName == null && whereArguments == null), 29 | 'Cannot specify both `whereSettings` and `whereName` or `whereArguments`', 30 | ); 31 | 32 | return _RouteMatcher( 33 | whereSettings: whereSettings, 34 | whereName: whereName, 35 | whereArguments: whereArguments, 36 | whereMaintainState: whereMaintainState, 37 | whereFullscreenDialog: whereFullscreenDialog, 38 | ); 39 | } 40 | 41 | /// Returns a matcher that matches the [RouteSettings] from the given [route]. 42 | Matcher equalsSettingsOf(Route route) { 43 | return isA() 44 | .having((s) => s.name, 'name', equals(route.settings.name)) 45 | .having( 46 | (s) => s.arguments, 47 | 'arguments', 48 | equals(route.settings.arguments), 49 | ); 50 | } 51 | 52 | class _RouteMatcher extends Matcher { 53 | const _RouteMatcher({ 54 | this.whereSettings, 55 | this.whereName, 56 | this.whereArguments, 57 | this.whereMaintainState, 58 | this.whereFullscreenDialog, 59 | }); 60 | 61 | final Matcher? whereSettings; 62 | final Matcher? whereName; 63 | final Matcher? whereArguments; 64 | final Matcher? whereMaintainState; 65 | final Matcher? whereFullscreenDialog; 66 | 67 | bool get hasTypeArgument => T != dynamic; 68 | 69 | bool get hasSettingsMatcher => whereSettings != null; 70 | 71 | bool get hasNameMatcher => whereName != null; 72 | 73 | bool get hasArgumentsMatcher => whereArguments != null; 74 | 75 | bool get hasMaintainStateMatcher => whereMaintainState != null; 76 | 77 | bool get hasFullscreenDialogMatcher => whereFullscreenDialog != null; 78 | 79 | bool get hasAnyMatchers => 80 | hasSettingsMatcher || 81 | hasNameMatcher || 82 | hasArgumentsMatcher || 83 | hasMaintainStateMatcher || 84 | hasFullscreenDialogMatcher; 85 | 86 | /// Takes an [input] string that looks like `FooBarRoute` and extracts 87 | /// the part `MyType`. 88 | /// 89 | /// If the `Route<` part cannot be found, it returns the input string 90 | /// unchaged. 91 | /// 92 | /// If generic types are nested, they will be captured as well. 93 | /// 94 | /// ```dart 95 | /// _extractRouteTypeArgument("FooRoute") == "A" 96 | /// _extractRouteTypeArgument("BarRoute>") == "A" 97 | /// _extractRouteTypeArgument("BazRoute>>") == "A>" 98 | /// 99 | /// _extractRouteTypeArgument("MyThing") == "MyThing" 100 | /// ``` 101 | String _extractRouteTypeArgument(String input) { 102 | const routeTypeString = 'Route<'; 103 | final routeTypeIndex = input.indexOf(routeTypeString); 104 | if (routeTypeIndex == -1) { 105 | return input; 106 | } 107 | 108 | final startIndex = routeTypeIndex + routeTypeString.length; 109 | final endIndex = input.lastIndexOf('>'); 110 | return input.substring(startIndex, endIndex); 111 | } 112 | 113 | /// Joins the given strings using a comma and space. 114 | /// The last two strings are joined using "and". 115 | /// 116 | /// ```dart 117 | /// _naturallyJoin(['a', 'b', 'c', 'd']) == 'a, b, c and d' 118 | /// ``` 119 | String _naturallyJoin(List strings) { 120 | if (strings.isEmpty) { 121 | return ''; 122 | } else if (strings.length == 1) { 123 | return strings[0]; 124 | } else if (strings.length == 2) { 125 | return '${strings[0]} and ${strings[1]}'; 126 | } else { 127 | return '${strings[0]}, ${_naturallyJoin(strings.sublist(1))}'; 128 | } 129 | } 130 | 131 | @override 132 | Description describe(Description description) { 133 | var dsc = description.add('a route'); 134 | if (hasTypeArgument) { 135 | dsc = dsc.add(' of type `$T`'); 136 | } 137 | 138 | if (hasAnyMatchers) { 139 | final matcherDescriptions = []; 140 | 141 | if (hasSettingsMatcher) { 142 | matcherDescriptions.add( 143 | '`settings` is ${whereSettings!.describeAsString()}', 144 | ); 145 | } 146 | if (hasNameMatcher) { 147 | matcherDescriptions.add( 148 | "the route's `name` is ${whereName!.describeAsString()}", 149 | ); 150 | } 151 | if (hasArgumentsMatcher) { 152 | matcherDescriptions.add( 153 | "the route's `arguments` is ${whereArguments!.describeAsString()}", 154 | ); 155 | } 156 | if (hasMaintainStateMatcher) { 157 | matcherDescriptions.add( 158 | '`maintainState` is ${whereMaintainState!.describeAsString()}', 159 | ); 160 | } 161 | if (hasFullscreenDialogMatcher) { 162 | matcherDescriptions.add( 163 | '`fullscreenDialog` is ${whereFullscreenDialog!.describeAsString()}', 164 | ); 165 | } 166 | 167 | if (matcherDescriptions.isNotEmpty) { 168 | dsc = dsc.add(' where ${_naturallyJoin(matcherDescriptions)}'); 169 | } 170 | } 171 | 172 | return dsc; 173 | } 174 | 175 | @override 176 | bool matches(dynamic item, Map matchState) { 177 | if (item is Route) { 178 | final typeMatches = !hasTypeArgument || item is Route; 179 | 180 | final settingsMatches = 181 | !hasSettingsMatcher || 182 | whereSettings!.matches(item.settings, matchState); 183 | final nameMatches = 184 | !hasNameMatcher || whereName!.matches(item.settings.name, matchState); 185 | final argumentsMatches = 186 | !hasArgumentsMatcher || 187 | whereArguments!.matches(item.settings.arguments, matchState); 188 | final maintainStateMatches = 189 | !hasMaintainStateMatcher || 190 | (item is ModalRoute && 191 | whereMaintainState!.matches(item.maintainState, matchState)); 192 | final fullscreenDialogMatches = 193 | !hasFullscreenDialogMatcher || 194 | (item is PageRoute && 195 | whereFullscreenDialog!.matches( 196 | item.fullscreenDialog, 197 | matchState, 198 | )); 199 | 200 | return typeMatches && 201 | settingsMatches && 202 | nameMatches && 203 | argumentsMatches && 204 | maintainStateMatches && 205 | fullscreenDialogMatches; 206 | } else { 207 | return false; 208 | } 209 | } 210 | 211 | @override 212 | Description describeMismatch( 213 | dynamic item, 214 | Description mismatchDescription, 215 | Map matchState, 216 | bool verbose, 217 | ) { 218 | if (item is! Route) { 219 | return mismatchDescription.add( 220 | 'is not a route but ' 221 | 'an instance of `${item.runtimeType}`', 222 | ); 223 | } 224 | 225 | final typeMatches = !hasTypeArgument || item is Route; 226 | 227 | final settingsMatches = 228 | !hasSettingsMatcher || 229 | whereSettings!.matches(item.settings, matchState); 230 | final nameMatches = 231 | !hasNameMatcher || whereName!.matches(item.settings.name, matchState); 232 | final argumentsMatches = 233 | !hasArgumentsMatcher || 234 | whereArguments!.matches(item.settings.arguments, matchState); 235 | final maintainStateMatches = 236 | !hasMaintainStateMatcher || 237 | (item is ModalRoute && 238 | whereMaintainState!.matches(item.maintainState, matchState)); 239 | final fullscreenDialogMatches = 240 | !hasFullscreenDialogMatcher || 241 | (item is PageRoute && 242 | whereFullscreenDialog!.matches(item.fullscreenDialog, matchState)); 243 | 244 | var dsc = mismatchDescription.add('is a route'); 245 | 246 | if (!typeMatches) { 247 | final routeType = _extractRouteTypeArgument(item.runtimeType.toString()); 248 | dsc = dsc.add(' of type `$routeType` instead of `$T`'); 249 | } 250 | 251 | if (hasAnyMatchers) { 252 | final mismatchDescriptions = []; 253 | 254 | if (!settingsMatches) { 255 | final mismatch = whereSettings!.describeMismatchAsString( 256 | item.settings, 257 | matchState, 258 | verbose: verbose, 259 | ); 260 | mismatchDescriptions.add('`settings` $mismatch'); 261 | } 262 | if (!nameMatches) { 263 | final name = item.settings.name; 264 | if (name == null || name.isEmpty) { 265 | mismatchDescriptions.add( 266 | "the route's `name` is empty " 267 | 'instead of ${whereName!.describeAsString()}', 268 | ); 269 | } else { 270 | final mismatch = whereName!.describeMismatchAsString( 271 | name, 272 | matchState, 273 | verbose: verbose, 274 | ); 275 | mismatchDescriptions.add("the route's `name` $mismatch"); 276 | } 277 | } 278 | if (!argumentsMatches) { 279 | final mismatch = whereArguments!.describeMismatchAsString( 280 | item.settings.arguments, 281 | matchState, 282 | verbose: verbose, 283 | ); 284 | mismatchDescriptions.add("the route's `arguments` $mismatch"); 285 | } 286 | if (!maintainStateMatches) { 287 | final mismatch = item is! ModalRoute 288 | ? 'is not a property on `${item.runtimeType}` and can only be ' 289 | 'used with `ModalRoute`s' 290 | : whereMaintainState!.describeMismatchAsString( 291 | item.maintainState, 292 | matchState, 293 | verbose: verbose, 294 | ); 295 | mismatchDescriptions.add('`maintainState` $mismatch'); 296 | } 297 | if (!fullscreenDialogMatches) { 298 | final mismatch = item is! PageRoute 299 | ? 'is not a property on `${item.runtimeType}` and can only be ' 300 | 'used with `PageRoute`s' 301 | : whereFullscreenDialog!.describeMismatchAsString( 302 | item.fullscreenDialog, 303 | matchState, 304 | verbose: verbose, 305 | ); 306 | mismatchDescriptions.add('`fullscreenDialog` $mismatch'); 307 | } 308 | 309 | if (mismatchDescriptions.length == 1) { 310 | dsc = dsc.add(' where ${mismatchDescriptions.first}'); 311 | } else if (mismatchDescriptions.isNotEmpty) { 312 | final mismatches = mismatchDescriptions.map((m) => '- $m').join('\n'); 313 | dsc = dsc.add(' where\n$mismatches'); 314 | } 315 | } 316 | 317 | return dsc; 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /lib/src/mock_navigator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:mocktail/mocktail.dart'; 3 | 4 | class _MockMaterialPageRoute extends MaterialPageRoute { 5 | _MockMaterialPageRoute({required super.builder}); 6 | 7 | void hackOverlays() { 8 | for (var i = 0; i < overlayEntries.length; i++) { 9 | // Entry can only be inserted when the state is mounted 10 | final state = _MockOverlayState().._mounted = true; 11 | final entry = OverlayEntry(builder: (_) => const SizedBox()); 12 | try { 13 | // We need to call insert since that is the only way to populate the 14 | // `_overlay` field in the entry. But that method calls a setState, 15 | // which will fail since we are not in a widget tree. 16 | // 17 | // By the time the setState is called, the attribute is already set 18 | // so we just ignore the error and the hack will do its job. 19 | state.insert(entry); 20 | } on Object catch (_) {} 21 | // Set mounted back to false to make sure the state doesn't get 22 | // marked as dirty during OverlayEntry.remove(). 23 | state._mounted = false; 24 | overlayEntries[i] = entry; 25 | } 26 | } 27 | } 28 | 29 | class _MockOverlayState extends OverlayState { 30 | late bool _mounted; 31 | 32 | @override 33 | bool get mounted => _mounted; 34 | } 35 | 36 | class _FakeRoute extends Fake implements Route {} 37 | 38 | /// {@template mock_navigator_provider} 39 | /// The widget that provides an instance of a [MockNavigator]. 40 | /// {@endtemplate} 41 | class MockNavigatorProvider extends Navigator { 42 | /// {@macro mock_navigator_provider} 43 | const MockNavigatorProvider({ 44 | required this.navigator, 45 | required this.child, 46 | super.key, 47 | }); 48 | 49 | /// The mock navigator used to mock navigation calls. 50 | final MockNavigator navigator; 51 | 52 | /// The [Widget] to render. 53 | final Widget child; 54 | 55 | @override 56 | NavigatorState createState() { 57 | // The hack that makes it all work. 58 | // ignore: no_logic_in_create_state 59 | return _MockNavigatorState(navigator).._child = child; 60 | } 61 | 62 | @override 63 | RouteFactory? get onGenerateRoute { 64 | return (_) { 65 | final route = _MockMaterialPageRoute(builder: (_) => child); 66 | 67 | navigator._routes.add(route); 68 | 69 | return route; 70 | }; 71 | } 72 | } 73 | 74 | /// {@template mock_navigator} 75 | /// A mock navigator which can be used to stub navigation for testing purposes. 76 | /// {@endtemplate} 77 | class MockNavigator extends Mock 78 | with _MockNavigatorDiagnosticsMixin 79 | implements NavigatorState { 80 | /// {@macro mock_navigator} 81 | MockNavigator() { 82 | registerFallbackValue(_FakeRoute()); 83 | registerFallbackValue(_FakeRoute()); 84 | registerFallbackValue(_FakeRoute()); 85 | registerFallbackValue(_FakeRoute()); 86 | registerFallbackValue(_FakeRoute()); 87 | registerFallbackValue(_FakeRoute()); 88 | } 89 | 90 | final _routes = <_MockMaterialPageRoute>[]; 91 | } 92 | 93 | /// A mixin necessary when implementing a [MockNavigator]. 94 | mixin _MockNavigatorDiagnosticsMixin on Object { 95 | @override 96 | String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { 97 | return super.toString(); 98 | } 99 | } 100 | 101 | /// Internal class that imitates a [NavigatorState] and maps all the real 102 | /// [NavigatorState] methods to the mock methods for use in testing. 103 | /// 104 | /// Any public method of [NavigatorState] used for routing should be overridden 105 | /// and remapped to the internal [_navigator] before any `verify` or `when` 106 | /// calls can function. 107 | class _MockNavigatorState extends NavigatorState { 108 | _MockNavigatorState(this._navigator); 109 | 110 | final MockNavigator _navigator; 111 | 112 | late Widget _child; 113 | 114 | @override 115 | Widget build(BuildContext context) => _child; 116 | 117 | @override 118 | void dispose() { 119 | for (final route in _navigator._routes) { 120 | route.hackOverlays(); 121 | } 122 | 123 | super.dispose(); 124 | } 125 | 126 | @override 127 | Future push(Route route) { 128 | return _navigator.push(route); 129 | } 130 | 131 | @override 132 | Future pushNamed( 133 | String routeName, { 134 | Object? arguments, 135 | }) { 136 | return _navigator.pushNamed(routeName, arguments: arguments); 137 | } 138 | 139 | @override 140 | Future pushNamedAndRemoveUntil( 141 | String newRouteName, 142 | RoutePredicate predicate, { 143 | Object? arguments, 144 | }) { 145 | return _navigator.pushNamedAndRemoveUntil( 146 | newRouteName, 147 | predicate, 148 | arguments: arguments, 149 | ); 150 | } 151 | 152 | @override 153 | Future pushReplacement( 154 | Route newRoute, { 155 | TO? result, 156 | }) { 157 | return _navigator.pushReplacement(newRoute, result: result); 158 | } 159 | 160 | @override 161 | Future pushReplacementNamed( 162 | String routeName, { 163 | TO? result, 164 | Object? arguments, 165 | }) { 166 | return _navigator.pushReplacementNamed( 167 | routeName, 168 | result: result, 169 | arguments: arguments, 170 | ); 171 | } 172 | 173 | @override 174 | void pop([T? result]) { 175 | return _navigator.pop(result); 176 | } 177 | 178 | @override 179 | Future popAndPushNamed( 180 | String routeName, { 181 | TO? result, 182 | Object? arguments, 183 | }) { 184 | return _navigator.popAndPushNamed( 185 | routeName, 186 | result: result, 187 | arguments: arguments, 188 | ); 189 | } 190 | 191 | @override 192 | void popUntil(RoutePredicate predicate) { 193 | return _navigator.popUntil(predicate); 194 | } 195 | 196 | @override 197 | bool canPop() { 198 | return _navigator.canPop(); 199 | } 200 | 201 | @override 202 | Future maybePop([T? result]) { 203 | return _navigator.maybePop(result); 204 | } 205 | 206 | @override 207 | Future pushAndRemoveUntil( 208 | Route newRoute, 209 | RoutePredicate predicate, 210 | ) { 211 | return _navigator.pushAndRemoveUntil(newRoute, predicate); 212 | } 213 | 214 | @override 215 | String restorablePopAndPushNamed( 216 | String routeName, { 217 | TO? result, 218 | Object? arguments, 219 | }) { 220 | return _navigator.restorablePopAndPushNamed( 221 | routeName, 222 | result: result, 223 | arguments: arguments, 224 | ); 225 | } 226 | 227 | @override 228 | String restorablePush( 229 | RestorableRouteBuilder routeBuilder, { 230 | Object? arguments, 231 | }) { 232 | return _navigator.restorablePush(routeBuilder, arguments: arguments); 233 | } 234 | 235 | @override 236 | String restorablePushAndRemoveUntil( 237 | RestorableRouteBuilder newRouteBuilder, 238 | RoutePredicate predicate, { 239 | Object? arguments, 240 | }) { 241 | return _navigator.restorablePushAndRemoveUntil( 242 | newRouteBuilder, 243 | predicate, 244 | arguments: arguments, 245 | ); 246 | } 247 | 248 | @override 249 | String restorablePushNamed( 250 | String routeName, { 251 | Object? arguments, 252 | }) { 253 | return _navigator.restorablePushNamed(routeName, arguments: arguments); 254 | } 255 | 256 | @override 257 | String restorablePushNamedAndRemoveUntil( 258 | String newRouteName, 259 | RoutePredicate predicate, { 260 | Object? arguments, 261 | }) { 262 | return _navigator.restorablePushNamedAndRemoveUntil( 263 | newRouteName, 264 | predicate, 265 | arguments: arguments, 266 | ); 267 | } 268 | 269 | @override 270 | String restorablePushReplacement( 271 | RestorableRouteBuilder routeBuilder, { 272 | TO? result, 273 | Object? arguments, 274 | }) { 275 | return _navigator.restorablePushReplacement( 276 | routeBuilder, 277 | result: result, 278 | arguments: arguments, 279 | ); 280 | } 281 | 282 | @override 283 | String restorablePushReplacementNamed( 284 | String routeName, { 285 | TO? result, 286 | Object? arguments, 287 | }) { 288 | return _navigator.restorablePushReplacementNamed( 289 | routeName, 290 | result: result, 291 | arguments: arguments, 292 | ); 293 | } 294 | 295 | @override 296 | String restorableReplace({ 297 | required Route oldRoute, 298 | required RestorableRouteBuilder newRouteBuilder, 299 | Object? arguments, 300 | }) { 301 | return _navigator.restorableReplace( 302 | oldRoute: oldRoute, 303 | newRouteBuilder: newRouteBuilder, 304 | arguments: arguments, 305 | ); 306 | } 307 | 308 | @override 309 | String restorableReplaceRouteBelow({ 310 | required Route anchorRoute, 311 | required RestorableRouteBuilder newRouteBuilder, 312 | Object? arguments, 313 | }) { 314 | return _navigator.restorableReplaceRouteBelow( 315 | anchorRoute: anchorRoute, 316 | newRouteBuilder: newRouteBuilder, 317 | arguments: arguments, 318 | ); 319 | } 320 | 321 | @override 322 | void removeRoute(Route route, [T? result]) { 323 | return _navigator.removeRoute(route, result); 324 | } 325 | 326 | @override 327 | void removeRouteBelow(Route anchorRoute, [T? result]) { 328 | return _navigator.removeRouteBelow(anchorRoute, result); 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: mockingjay 2 | description: A package that makes it easy to mock, test and verify navigation calls in Flutter. 3 | version: 2.0.0 4 | homepage: https://github.com/VeryGoodOpenSource/mockingjay 5 | 6 | environment: 7 | sdk: ^3.8.0 8 | flutter: ">=3.32.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | flutter_test: 14 | sdk: flutter 15 | matcher: ^0.12.17 16 | mocktail: ^1.0.4 17 | test: ^1.25.7 18 | 19 | dev_dependencies: 20 | very_good_analysis: ^8.0.0 21 | -------------------------------------------------------------------------------- /test/src/example_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockingjay/mockingjay.dart'; 4 | 5 | class MyHomePage extends StatelessWidget { 6 | const MyHomePage({super.key}); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Scaffold( 11 | body: TextButton( 12 | onPressed: () => Navigator.of(context).push(MySettingsPage.route()), 13 | child: const Text('Navigate'), 14 | ), 15 | ); 16 | } 17 | } 18 | 19 | class MySettingsPage extends StatelessWidget { 20 | const MySettingsPage({super.key}); 21 | 22 | static Route route() { 23 | return MaterialPageRoute( 24 | builder: (_) => const MySettingsPage(), 25 | settings: const RouteSettings(name: '/settings'), 26 | ); 27 | } 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | return const Scaffold(); 32 | } 33 | } 34 | 35 | void main() { 36 | testWidgets('pushes SettingsPage when TextButton is tapped', (tester) async { 37 | final navigator = MockNavigator(); 38 | when(navigator.canPop).thenReturn(true); 39 | when(() => navigator.push(any())).thenAnswer((_) async {}); 40 | 41 | await tester.pumpWidget( 42 | MaterialApp( 43 | home: MockNavigatorProvider( 44 | navigator: navigator, 45 | child: const MyHomePage(), 46 | ), 47 | ), 48 | ); 49 | 50 | await tester.tap(find.byType(TextButton)); 51 | 52 | verify( 53 | () => navigator.push( 54 | any(that: isRoute(whereName: equals('/settings'))), 55 | ), 56 | ).called(1); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /test/src/matchers_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockingjay/mockingjay.dart'; 4 | 5 | class NonModalRoute extends Mock implements TransitionRoute {} 6 | 7 | Future expectToFail( 8 | dynamic actual, 9 | Matcher matcher, { 10 | required String withMessage, 11 | }) async { 12 | var didNotFail = false; 13 | try { 14 | await expectLater(actual, matcher); 15 | didNotFail = true; 16 | } on TestFailure catch (error) { 17 | const whichClause = ' Which: '; 18 | final whichClauseIndex = error.message!.indexOf(whichClause); 19 | final reasonIndex = whichClauseIndex + whichClause.length; 20 | final reason = error.message!.substring(reasonIndex).trim(); 21 | 22 | expect(reason, equals(withMessage)); 23 | } 24 | 25 | if (didNotFail) { 26 | fail('TestFailure expected but not thrown'); 27 | } 28 | } 29 | 30 | void main() { 31 | group('Matchers', () { 32 | group('isRoute', () { 33 | Route createRoute({ 34 | String? name, 35 | Object? arguments, 36 | bool maintainState = true, 37 | bool fullscreenDialog = false, 38 | }) { 39 | return MaterialPageRoute( 40 | settings: RouteSettings(name: name, arguments: arguments), 41 | maintainState: maintainState, 42 | fullscreenDialog: fullscreenDialog, 43 | builder: (_) => const SizedBox(), 44 | ); 45 | } 46 | 47 | group('constructor', () { 48 | test('throws AssertionError when both whereSettings ' 49 | 'and whereName or whereArguments matchers are provided', () { 50 | expect( 51 | () => isRoute( 52 | whereSettings: isNotNull, 53 | whereName: isNotNull, 54 | whereArguments: isNotNull, 55 | ), 56 | throwsAssertionError, 57 | ); 58 | }); 59 | }); 60 | 61 | group('without arguments', () { 62 | test('matches any route', () { 63 | expect(createRoute(), isRoute()); 64 | expect(createRoute(), isRoute()); 65 | expect(createRoute(name: '/test'), isRoute()); 66 | expect(createRoute(name: '/test'), isRoute()); 67 | }); 68 | 69 | test('does not match anything that is not a route', () { 70 | expectToFail( 71 | 1, 72 | isRoute(), 73 | withMessage: 'is not a route but an instance of `int`', 74 | ); 75 | expectToFail( 76 | 'a', 77 | isRoute(), 78 | withMessage: 'is not a route but an instance of `String`', 79 | ); 80 | expectToFail( 81 | null, 82 | isRoute(), 83 | withMessage: 'is not a route but an instance of `Null`', 84 | ); 85 | expectToFail( 86 | const SizedBox(), 87 | isRoute(), 88 | withMessage: 'is not a route but an instance of `SizedBox`', 89 | ); 90 | }); 91 | }); 92 | 93 | group('with type argument', () { 94 | test('matches any route of correct type', () { 95 | expect(createRoute(), isRoute()); 96 | expect(createRoute(name: '/test'), isRoute()); 97 | }); 98 | 99 | test('does not match anything that is not a route of that type', () { 100 | expectToFail( 101 | createRoute(), 102 | isRoute(), 103 | withMessage: 'is a route of type `dynamic` instead of `String`', 104 | ); 105 | expectToFail( 106 | createRoute(name: '/test'), 107 | isRoute(), 108 | withMessage: 'is a route of type `dynamic` instead of `String`', 109 | ); 110 | expectToFail( 111 | 1, 112 | isRoute(), 113 | withMessage: 'is not a route but an instance of `int`', 114 | ); 115 | }); 116 | }); 117 | 118 | group('with whereSettings argument', () { 119 | test('matches any route with matching settings', () { 120 | expect( 121 | createRoute(), 122 | isRoute(whereSettings: equalsSettingsOf(createRoute())), 123 | ); 124 | expect( 125 | createRoute(name: '/test'), 126 | isRoute( 127 | whereSettings: isA().having( 128 | (s) => s.name, 129 | 'name', 130 | '/test', 131 | ), 132 | ), 133 | ); 134 | }); 135 | 136 | test('does not match anything that is not a route ' 137 | 'with matching settings', () { 138 | expectToFail( 139 | createRoute(name: '/test'), 140 | isRoute(whereSettings: equalsSettingsOf(createRoute())), 141 | withMessage: 142 | "is a route where `settings` has `name` with value '/test'", 143 | ); 144 | expectToFail( 145 | createRoute(name: '/other_name'), 146 | isRoute( 147 | whereSettings: equalsSettingsOf( 148 | createRoute(name: '/test'), 149 | ), 150 | ), 151 | withMessage: ''' 152 | is a route where `settings` has `name` with value '/other_name' which is different. 153 | Expected: /test 154 | Actual: /other_name ... 155 | ^ 156 | Differ at offset 1''', 157 | ); 158 | expectToFail( 159 | 1, 160 | isRoute(whereSettings: equalsSettingsOf(createRoute())), 161 | withMessage: 'is not a route but an instance of `int`', 162 | ); 163 | }); 164 | }); 165 | 166 | group('with whereName argument', () { 167 | test('matches any route with correct name', () { 168 | expect( 169 | createRoute(name: '/test'), 170 | isRoute(whereName: equals('/test')), 171 | ); 172 | expect( 173 | createRoute(name: '/test'), 174 | isRoute(whereName: equals('/test')), 175 | ); 176 | }); 177 | 178 | test('does not match anything that is not a route with that name', () { 179 | expectToFail( 180 | createRoute(), 181 | isRoute(whereName: equals('/test')), 182 | withMessage: 183 | "is a route where the route's `name` is empty instead of '/test'", 184 | ); 185 | expectToFail( 186 | createRoute(name: '/other_name'), 187 | isRoute(whereName: equals('/test')), 188 | withMessage: ''' 189 | is a route where the route's `name` is different. 190 | Expected: /test 191 | Actual: /other_name ... 192 | ^ 193 | Differ at offset 1''', 194 | ); 195 | expectToFail( 196 | 1, 197 | isRoute(whereName: equals('/test')), 198 | withMessage: 'is not a route but an instance of `int`', 199 | ); 200 | }); 201 | }); 202 | 203 | group('with whereArguments argument', () { 204 | test('matches any route with correct arguments', () { 205 | expect( 206 | createRoute(arguments: {'a': 1}), 207 | isRoute(whereArguments: equals({'a': 1})), 208 | ); 209 | expect( 210 | createRoute(arguments: {'a': 1}), 211 | isRoute(whereArguments: equals({'a': 1})), 212 | ); 213 | }); 214 | 215 | test( 216 | 'does not match anything that is not a route with same arguments', 217 | () { 218 | expectToFail( 219 | createRoute(arguments: {'a': 1}), 220 | isRoute(whereArguments: equals({'a': 2})), 221 | withMessage: 222 | "is a route where the route's `arguments` " 223 | "at location ['a'] is <1> instead of <2>", 224 | ); 225 | expectToFail( 226 | createRoute(arguments: {'a': 1}), 227 | isRoute(whereArguments: equals({'b': 1})), 228 | withMessage: 229 | "is a route where the route's `arguments` " 230 | "is missing map key 'b'", 231 | ); 232 | expectToFail( 233 | 1, 234 | isRoute(whereArguments: equals({'a': 1})), 235 | withMessage: 'is not a route but an instance of `int`', 236 | ); 237 | }, 238 | ); 239 | }); 240 | 241 | group('with whereMaintainState argument', () { 242 | test('matches any route with matching maintainState argument', () { 243 | expect(createRoute(), isRoute(whereMaintainState: isTrue)); 244 | }); 245 | 246 | test('does not match anything that is not a route with matching ' 247 | 'maintainState argument', () { 248 | expectToFail( 249 | createRoute(), 250 | isRoute(whereMaintainState: isFalse), 251 | withMessage: 252 | 'is a route where `maintainState` ' 253 | 'is true instead of false', 254 | ); 255 | expectToFail( 256 | NonModalRoute(), 257 | isRoute(whereMaintainState: isTrue), 258 | withMessage: 259 | 'is a route where `maintainState` ' 260 | 'is not a property on `NonModalRoute` and can only be used ' 261 | 'with `ModalRoute`s', 262 | ); 263 | expectToFail( 264 | 1, 265 | isRoute(whereMaintainState: isTrue), 266 | withMessage: 'is not a route but an instance of `int`', 267 | ); 268 | }); 269 | }); 270 | 271 | group('with whereFullscreenDialog argument', () { 272 | test('matches any route with matching fullscreenDialog argument', () { 273 | expect( 274 | createRoute(fullscreenDialog: true), 275 | isRoute(whereFullscreenDialog: isTrue), 276 | ); 277 | }); 278 | 279 | test('does not match anything that is not a route with matching ' 280 | 'fullscreenDialog argument', () { 281 | expectToFail( 282 | createRoute(fullscreenDialog: true), 283 | isRoute(whereFullscreenDialog: isFalse), 284 | withMessage: 285 | 'is a route where `fullscreenDialog` ' 286 | 'is true instead of false', 287 | ); 288 | expectToFail( 289 | NonModalRoute(), 290 | isRoute(whereFullscreenDialog: isFalse), 291 | withMessage: 292 | 'is a route where `fullscreenDialog` ' 293 | 'is not a property on `NonModalRoute` and can only be used ' 294 | 'with `PageRoute`s', 295 | ); 296 | expectToFail( 297 | 1, 298 | isRoute(whereFullscreenDialog: isTrue), 299 | withMessage: 'is not a route but an instance of `int`', 300 | ); 301 | }); 302 | }); 303 | 304 | test('returns all relevant mismatches in one log', () { 305 | expectToFail( 306 | createRoute( 307 | name: '/other_name', 308 | arguments: {'b': 1}, 309 | maintainState: false, 310 | fullscreenDialog: true, 311 | ), 312 | isRoute( 313 | whereName: equals('/test'), 314 | whereArguments: equals({'a': 1}), 315 | whereMaintainState: isTrue, 316 | whereFullscreenDialog: isFalse, 317 | ), 318 | withMessage: ''' 319 | is a route where 320 | - the route's `name` is different. 321 | Expected: /test 322 | Actual: /other_name ... 323 | ^ 324 | Differ at offset 1 325 | - the route's `arguments` is missing map key 'a' 326 | - `maintainState` is false instead of true 327 | - `fullscreenDialog` is true instead of false''', 328 | ); 329 | }); 330 | }); 331 | }); 332 | } 333 | -------------------------------------------------------------------------------- /test/src/mock_navigator_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | import 'package:mockingjay/mockingjay.dart'; 5 | 6 | extension on WidgetTester { 7 | Future pumpTest({ 8 | required MockNavigator navigator, 9 | required WidgetBuilder builder, 10 | }) async { 11 | await pumpWidget( 12 | MaterialApp( 13 | title: 'Mock Navigator Test', 14 | home: Scaffold( 15 | body: MockNavigatorProvider( 16 | navigator: navigator, 17 | child: Builder(builder: builder), 18 | ), 19 | ), 20 | ), 21 | ); 22 | } 23 | } 24 | 25 | void main() { 26 | group('MockNavigator', () { 27 | late MockNavigator navigator; 28 | 29 | const testRouteName = '__test_route__'; 30 | final testRoute = MaterialPageRoute( 31 | settings: const RouteSettings(name: testRouteName), 32 | builder: (_) => const Text(testRouteName), 33 | ); 34 | bool testRoutePredicate(Route _) => false; 35 | Route restorableTestRouteBuilder( 36 | BuildContext context, 37 | Object? arguments, 38 | ) { 39 | return testRoute; 40 | } 41 | 42 | setUp(() { 43 | navigator = MockNavigator(); 44 | when(() => navigator.canPop()).thenReturn(true); 45 | }); 46 | 47 | test('toString returns normally', () { 48 | expect(() => navigator.toString(), returnsNormally); 49 | }); 50 | 51 | testWidgets('mocks .push calls', (tester) async { 52 | when(() => navigator.push(any())).thenAnswer((_) async {}); 53 | 54 | await tester.pumpTest( 55 | navigator: navigator, 56 | builder: (context) => TextButton( 57 | onPressed: () => Navigator.of(context).push(testRoute), 58 | child: const Text('Trigger'), 59 | ), 60 | ); 61 | 62 | await tester.tap(find.byType(TextButton)); 63 | verify(() => navigator.push(testRoute)).called(1); 64 | }); 65 | 66 | testWidgets('mocks .pushNamed calls', (tester) async { 67 | when(() => navigator.pushNamed(any())).thenAnswer((_) async => null); 68 | 69 | await tester.pumpTest( 70 | navigator: navigator, 71 | builder: (context) => TextButton( 72 | onPressed: () => Navigator.of(context).pushNamed(testRouteName), 73 | child: const Text('Trigger'), 74 | ), 75 | ); 76 | 77 | await tester.tap(find.byType(TextButton)); 78 | verify(() => navigator.pushNamed(testRouteName)).called(1); 79 | }); 80 | 81 | testWidgets('mocks .pushNamedAndRemoveUntil calls', (tester) async { 82 | when( 83 | () => navigator.pushNamedAndRemoveUntil(any(), any()), 84 | ).thenAnswer((_) async => null); 85 | 86 | await tester.pumpTest( 87 | navigator: navigator, 88 | builder: (context) => TextButton( 89 | onPressed: () => Navigator.of( 90 | context, 91 | ).pushNamedAndRemoveUntil(testRouteName, testRoutePredicate), 92 | child: const Text('Trigger'), 93 | ), 94 | ); 95 | 96 | await tester.tap(find.byType(TextButton)); 97 | verify( 98 | () => navigator.pushNamedAndRemoveUntil( 99 | testRouteName, 100 | testRoutePredicate, 101 | ), 102 | ).called(1); 103 | }); 104 | 105 | testWidgets('mocks .pushReplacement calls', (tester) async { 106 | when( 107 | () => navigator.pushReplacement(any()), 108 | ).thenAnswer((_) async {}); 109 | 110 | await tester.pumpTest( 111 | navigator: navigator, 112 | builder: (context) => TextButton( 113 | onPressed: () => Navigator.of(context).pushReplacement(testRoute), 114 | child: const Text('Trigger'), 115 | ), 116 | ); 117 | 118 | await tester.tap(find.byType(TextButton)); 119 | verify(() => navigator.pushReplacement(testRoute)).called(1); 120 | }); 121 | 122 | testWidgets('mocks .pushReplacementNamed calls', (tester) async { 123 | when( 124 | () => navigator.pushReplacementNamed(any()), 125 | ).thenAnswer((_) async => null); 126 | 127 | await tester.pumpTest( 128 | navigator: navigator, 129 | builder: (context) => TextButton( 130 | onPressed: () => 131 | Navigator.of(context).pushReplacementNamed(testRouteName), 132 | child: const Text('Trigger'), 133 | ), 134 | ); 135 | 136 | await tester.tap(find.byType(TextButton)); 137 | verify(() => navigator.pushReplacementNamed(testRouteName)).called(1); 138 | }); 139 | 140 | testWidgets('mocks .pop calls', (tester) async { 141 | when(() => navigator.pop(any())).thenAnswer((_) async {}); 142 | 143 | await tester.pumpTest( 144 | navigator: navigator, 145 | builder: (context) => TextButton( 146 | onPressed: () => Navigator.of(context).pop(testRoute), 147 | child: const Text('Trigger'), 148 | ), 149 | ); 150 | 151 | await tester.tap(find.byType(TextButton)); 152 | verify(() => navigator.pop(testRoute)).called(1); 153 | }); 154 | 155 | testWidgets('mocks .popAndPushNamed calls', (tester) async { 156 | when( 157 | () => navigator.popAndPushNamed(any()), 158 | ).thenAnswer((_) async => null); 159 | 160 | await tester.pumpTest( 161 | navigator: navigator, 162 | builder: (context) => TextButton( 163 | onPressed: () => Navigator.of(context).popAndPushNamed(testRouteName), 164 | child: const Text('Trigger'), 165 | ), 166 | ); 167 | 168 | await tester.tap(find.byType(TextButton)); 169 | verify(() => navigator.popAndPushNamed(testRouteName)).called(1); 170 | }); 171 | 172 | testWidgets('mocks .popUntil calls', (tester) async { 173 | when(() => navigator.popUntil(any())).thenAnswer((_) async {}); 174 | 175 | await tester.pumpTest( 176 | navigator: navigator, 177 | builder: (context) => TextButton( 178 | onPressed: () => Navigator.of(context).popUntil(testRoutePredicate), 179 | child: const Text('Trigger'), 180 | ), 181 | ); 182 | 183 | await tester.tap(find.byType(TextButton)); 184 | verify(() => navigator.popUntil(testRoutePredicate)).called(1); 185 | }); 186 | 187 | testWidgets('mocks .canPop calls', (tester) async { 188 | when(() => navigator.canPop()).thenReturn(true); 189 | 190 | await tester.pumpTest( 191 | navigator: navigator, 192 | builder: (context) => TextButton( 193 | onPressed: () => Navigator.of(context).canPop(), 194 | child: const Text('Trigger'), 195 | ), 196 | ); 197 | 198 | // Called by NavigatorState.didChangeDependencies initially 199 | verify(() => navigator.canPop()).called(1); 200 | await tester.tap(find.byType(TextButton)); 201 | verify(() => navigator.canPop()).called(1); 202 | }); 203 | 204 | testWidgets('mocks .maybePop calls', (tester) async { 205 | when(() => navigator.maybePop()).thenAnswer((_) async => true); 206 | 207 | await tester.pumpTest( 208 | navigator: navigator, 209 | builder: (context) => TextButton( 210 | onPressed: () => Navigator.of(context).maybePop(), 211 | child: const Text('Trigger'), 212 | ), 213 | ); 214 | 215 | await tester.tap(find.byType(TextButton)); 216 | verify(() => navigator.maybePop()).called(1); 217 | }); 218 | 219 | testWidgets('mocks .maybePop calls w/result', (tester) async { 220 | when( 221 | () => navigator.maybePop(any()), 222 | ).thenAnswer((_) async => true); 223 | 224 | await tester.pumpTest( 225 | navigator: navigator, 226 | builder: (context) => TextButton( 227 | onPressed: () => Navigator.of(context).maybePop(true), 228 | child: const Text('Trigger'), 229 | ), 230 | ); 231 | 232 | await tester.tap(find.byType(TextButton)); 233 | verify(() => navigator.maybePop(true)).called(1); 234 | }); 235 | 236 | testWidgets('mocks .pushAndRemoveUntil calls', (tester) async { 237 | when( 238 | () => navigator.pushAndRemoveUntil(any(), any()), 239 | ).thenAnswer((_) async {}); 240 | 241 | await tester.pumpTest( 242 | navigator: navigator, 243 | builder: (context) => TextButton( 244 | onPressed: () => Navigator.of( 245 | context, 246 | ).pushAndRemoveUntil(testRoute, testRoutePredicate), 247 | child: const Text('Trigger'), 248 | ), 249 | ); 250 | 251 | await tester.tap(find.byType(TextButton)); 252 | verify( 253 | () => navigator.pushAndRemoveUntil(testRoute, testRoutePredicate), 254 | ).called(1); 255 | }); 256 | 257 | testWidgets('mocks .restorablePopAndPushNamed calls', (tester) async { 258 | when( 259 | () => navigator.restorablePopAndPushNamed(any()), 260 | ).thenReturn(testRouteName); 261 | 262 | await tester.pumpTest( 263 | navigator: navigator, 264 | builder: (context) => TextButton( 265 | onPressed: () => 266 | Navigator.of(context).restorablePopAndPushNamed(testRouteName), 267 | child: const Text('Trigger'), 268 | ), 269 | ); 270 | 271 | await tester.tap(find.byType(TextButton)); 272 | verify( 273 | () => navigator.restorablePopAndPushNamed(testRouteName), 274 | ).called(1); 275 | }); 276 | 277 | testWidgets('mocks .restorablePush calls', (tester) async { 278 | when( 279 | () => navigator.restorablePush(any()), 280 | ).thenReturn(testRouteName); 281 | 282 | await tester.pumpTest( 283 | navigator: navigator, 284 | builder: (context) => TextButton( 285 | onPressed: () => 286 | Navigator.of(context).restorablePush(restorableTestRouteBuilder), 287 | child: const Text('Trigger'), 288 | ), 289 | ); 290 | 291 | await tester.tap(find.byType(TextButton)); 292 | verify( 293 | () => navigator.restorablePush(restorableTestRouteBuilder), 294 | ).called(1); 295 | }); 296 | 297 | testWidgets('mocks .restorablePushAndRemoveUntil calls', (tester) async { 298 | when( 299 | () => navigator.restorablePushAndRemoveUntil(any(), any()), 300 | ).thenReturn(testRouteName); 301 | 302 | await tester.pumpTest( 303 | navigator: navigator, 304 | builder: (context) => TextButton( 305 | onPressed: () => Navigator.of(context).restorablePushAndRemoveUntil( 306 | restorableTestRouteBuilder, 307 | testRoutePredicate, 308 | ), 309 | child: const Text('Trigger'), 310 | ), 311 | ); 312 | 313 | await tester.tap(find.byType(TextButton)); 314 | verify( 315 | () => navigator.restorablePushAndRemoveUntil( 316 | restorableTestRouteBuilder, 317 | testRoutePredicate, 318 | ), 319 | ).called(1); 320 | }); 321 | 322 | testWidgets('mocks .restorablePushNamed calls', (tester) async { 323 | when( 324 | () => navigator.restorablePushNamed(any()), 325 | ).thenReturn(testRouteName); 326 | 327 | await tester.pumpTest( 328 | navigator: navigator, 329 | builder: (context) => TextButton( 330 | onPressed: () => 331 | Navigator.of(context).restorablePushNamed(testRouteName), 332 | child: const Text('Trigger'), 333 | ), 334 | ); 335 | 336 | await tester.tap(find.byType(TextButton)); 337 | verify(() => navigator.restorablePushNamed(testRouteName)).called(1); 338 | }); 339 | 340 | testWidgets('mocks .restorablePushNamedAndRemoveUntil calls', ( 341 | tester, 342 | ) async { 343 | when( 344 | () => navigator.restorablePushNamedAndRemoveUntil(any(), any()), 345 | ).thenReturn(testRouteName); 346 | 347 | await tester.pumpTest( 348 | navigator: navigator, 349 | builder: (context) => TextButton( 350 | onPressed: () => 351 | Navigator.of(context).restorablePushNamedAndRemoveUntil( 352 | testRouteName, 353 | testRoutePredicate, 354 | ), 355 | child: const Text('Trigger'), 356 | ), 357 | ); 358 | 359 | await tester.tap(find.byType(TextButton)); 360 | verify( 361 | () => navigator.restorablePushNamedAndRemoveUntil( 362 | testRouteName, 363 | testRoutePredicate, 364 | ), 365 | ).called(1); 366 | }); 367 | 368 | testWidgets('mocks .restorablePushReplacement calls', (tester) async { 369 | when( 370 | () => navigator.restorablePushReplacement(any()), 371 | ).thenReturn(testRouteName); 372 | 373 | await tester.pumpTest( 374 | navigator: navigator, 375 | builder: (context) => TextButton( 376 | onPressed: () => Navigator.of( 377 | context, 378 | ).restorablePushReplacement(restorableTestRouteBuilder), 379 | child: const Text('Trigger'), 380 | ), 381 | ); 382 | 383 | await tester.tap(find.byType(TextButton)); 384 | verify( 385 | () => navigator.restorablePushReplacement(restorableTestRouteBuilder), 386 | ).called(1); 387 | }); 388 | 389 | testWidgets('mocks .restorablePushReplacementNamed calls', (tester) async { 390 | when( 391 | () => navigator.restorablePushReplacementNamed(any()), 392 | ).thenReturn(testRouteName); 393 | 394 | await tester.pumpTest( 395 | navigator: navigator, 396 | builder: (context) => TextButton( 397 | onPressed: () => Navigator.of( 398 | context, 399 | ).restorablePushReplacementNamed(testRouteName), 400 | child: const Text('Trigger'), 401 | ), 402 | ); 403 | 404 | await tester.tap(find.byType(TextButton)); 405 | verify( 406 | () => navigator.restorablePushReplacementNamed(testRouteName), 407 | ).called(1); 408 | }); 409 | 410 | testWidgets('mocks .restorableReplace calls', (tester) async { 411 | when( 412 | () => navigator.restorableReplace( 413 | oldRoute: any(named: 'oldRoute'), 414 | newRouteBuilder: any(named: 'newRouteBuilder'), 415 | ), 416 | ).thenReturn(testRouteName); 417 | 418 | await tester.pumpTest( 419 | navigator: navigator, 420 | builder: (context) => TextButton( 421 | onPressed: () => Navigator.of(context).restorableReplace( 422 | oldRoute: testRoute, 423 | newRouteBuilder: restorableTestRouteBuilder, 424 | ), 425 | child: const Text('Trigger'), 426 | ), 427 | ); 428 | 429 | await tester.tap(find.byType(TextButton)); 430 | verify( 431 | () => navigator.restorableReplace( 432 | oldRoute: testRoute, 433 | newRouteBuilder: restorableTestRouteBuilder, 434 | ), 435 | ).called(1); 436 | }); 437 | 438 | testWidgets('mocks .restorableReplaceRouteBelow calls', (tester) async { 439 | when( 440 | () => navigator.restorableReplaceRouteBelow( 441 | anchorRoute: any(named: 'anchorRoute'), 442 | newRouteBuilder: any(named: 'newRouteBuilder'), 443 | ), 444 | ).thenReturn(testRouteName); 445 | 446 | await tester.pumpTest( 447 | navigator: navigator, 448 | builder: (context) => TextButton( 449 | onPressed: () => Navigator.of(context).restorableReplaceRouteBelow( 450 | anchorRoute: testRoute, 451 | newRouteBuilder: restorableTestRouteBuilder, 452 | ), 453 | child: const Text('Trigger'), 454 | ), 455 | ); 456 | 457 | await tester.tap(find.byType(TextButton)); 458 | verify( 459 | () => navigator.restorableReplaceRouteBelow( 460 | anchorRoute: testRoute, 461 | newRouteBuilder: restorableTestRouteBuilder, 462 | ), 463 | ).called(1); 464 | }); 465 | 466 | testWidgets('mocks .removeRoute calls', (tester) async { 467 | when( 468 | () => navigator.removeRoute(any(), any()), 469 | ).thenAnswer((_) {}); 470 | 471 | await tester.pumpTest( 472 | navigator: navigator, 473 | builder: (context) => TextButton( 474 | onPressed: () => Navigator.of(context).removeRoute(testRoute), 475 | child: const Text('Trigger'), 476 | ), 477 | ); 478 | 479 | await tester.tap(find.byType(TextButton)); 480 | 481 | verify(() => navigator.removeRoute(testRoute)).called(1); 482 | }); 483 | 484 | testWidgets('mocks .removeRouteBelow calls', (tester) async { 485 | when( 486 | () => navigator.removeRouteBelow(any(), any()), 487 | ).thenAnswer((_) {}); 488 | 489 | await tester.pumpTest( 490 | navigator: navigator, 491 | builder: (context) => TextButton( 492 | onPressed: () => Navigator.of(context).removeRouteBelow(testRoute), 493 | child: const Text('Trigger'), 494 | ), 495 | ); 496 | 497 | await tester.tap(find.byType(TextButton)); 498 | 499 | verify(() => navigator.removeRouteBelow(testRoute)).called(1); 500 | }); 501 | }); 502 | } 503 | -------------------------------------------------------------------------------- /tool/release_ready.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Ensures that the package is ready for a release. 4 | # 5 | # Will update the version.dart file and update the CHANGELOG.md. 6 | # 7 | # Set it up for a new version: 8 | # `sh tool/release_ready.sh ` 9 | 10 | # Check if current directory is usable for this script, if so we assume it is correctly set up. 11 | if [ ! -f "pubspec.yaml" ]; then 12 | echo "$(pwd) is not a valid Dart package." 13 | exit 1 14 | fi 15 | 16 | currentBranch=$(git symbolic-ref --short -q HEAD) 17 | if [[ ! $currentBranch == "main" ]]; then 18 | echo "Releasing is only supported on the main branch." 19 | exit 1 20 | fi 21 | 22 | # Get information 23 | old_version="" 24 | if [ -f "pubspec.yaml" ]; then 25 | old_version=$(dart pub deps --json | pcregrep -o1 -i '"version": "(.*?)"' | head -1) 26 | fi 27 | 28 | if [ -z "$old_version" ]; then 29 | echo "Current version was not resolved." 30 | exit 1 31 | fi 32 | 33 | # Get new version 34 | new_version="$1"; 35 | 36 | if [[ "$new_version" == "" ]]; then 37 | echo "No new version supplied, please provide one" 38 | exit 1 39 | fi 40 | 41 | if [[ "$new_version" == "$old_version" ]]; then 42 | echo "Current version is $old_version, can't update." 43 | exit 1 44 | fi 45 | 46 | # Retrieving all the commits in the current directory since the last tag. 47 | previousTag="v${old_version}" 48 | raw_commits="$(git log --pretty=format:"%s" --no-merges --reverse $previousTag..HEAD -- .)" 49 | markdown_commits=$(echo "$raw_commits" | sed -En "s/\(#([0-9]+)\)/([#\1](https:\/\/github.com\/VeryGoodOpenSource\/mockingjay\/pull\/\1))/p") 50 | 51 | if [[ "$markdown_commits" == "" ]]; then 52 | echo "No commits since last tag, can't update." 53 | exit 0 54 | fi 55 | commits=$(echo "$markdown_commits" | sed -En "s/^/- /p") 56 | 57 | echo "Updating version to $new_version" 58 | if [ -f "pubspec.yaml" ]; then 59 | sed -i '' "s/version: $old_version/version: $new_version/g" pubspec.yaml 60 | fi 61 | 62 | # Update dart file with new version. 63 | dart run build_runner build --delete-conflicting-outputs > /dev/null 64 | 65 | if grep -q v$new_version "CHANGELOG.md"; then 66 | echo "CHANGELOG already contains version $new_version." 67 | exit 1 68 | fi 69 | 70 | # Add a new version entry with the found commits to the CHANGELOG.md. 71 | echo "# ${new_version} \n\n ${commits}\n\n$(cat CHANGELOG.md)" > CHANGELOG.md 72 | echo "CHANGELOG generated, validate entries here: $(pwd)/CHANGELOG.md" 73 | 74 | echo "Creating git branch for mockingay@$new_version" 75 | git checkout -b "chore/$new_version" > /dev/null 76 | 77 | git add pubspec.yaml CHANGELOG.md 78 | if [ -f lib/src/version.dart ]; then 79 | git add lib/src/version.dart 80 | fi 81 | 82 | echo "" 83 | echo "Run the following command if you wish to commit the changes:" 84 | echo "git commit -m \"chore: v$new_version\"" -------------------------------------------------------------------------------- /tool/verify_pub_score.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Runs `pana . --no-warning` and verifies that the package score 3 | # is greater or equal to the desired score. By default the desired score is 4 | # a perfect score but it can be overridden by passing the desired score as an argument. 5 | # 6 | # Ensure the package has a score of at least a 100 7 | # `./verify_pub_score.sh 100` 8 | # 9 | # Ensure the package has a perfect score 10 | # `./verify_pub_score.sh` 11 | 12 | PANA=$(pana . --no-warning); PANA_SCORE=$(echo $PANA | sed -n "s/.*Points: \([0-9]*\)\/\([0-9]*\)./\1\/\2/p") 13 | echo "score: $PANA_SCORE" 14 | IFS='/'; read -a SCORE_ARR <<< "$PANA_SCORE"; SCORE=SCORE_ARR[0]; TOTAL=SCORE_ARR[1] 15 | if [ -z "$1" ]; then MINIMUM_SCORE=TOTAL; else MINIMUM_SCORE=$1; fi 16 | if (( $SCORE < $MINIMUM_SCORE )); then echo "minimum score $MINIMUM_SCORE was not met!"; exit 1; fi --------------------------------------------------------------------------------