├── .fvmrc ├── example ├── linux │ ├── .gitignore │ ├── main.cc │ ├── flutter │ │ ├── generated_plugin_registrant.cc │ │ ├── generated_plugin_registrant.h │ │ ├── generated_plugins.cmake │ │ └── CMakeLists.txt │ ├── my_application.h │ └── my_application.cc ├── lib │ ├── home │ │ └── home.dart │ ├── widgets │ │ ├── widgets.dart │ │ ├── red_button.dart │ │ └── contact_list_tile.dart │ ├── main.dart │ └── app │ │ └── app.dart ├── ios │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner │ │ ├── Runner-Bridging-Header.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── 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-1024x1024@1x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── Contents.json │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ └── .gitignore ├── macos │ ├── Flutter │ │ ├── Flutter-Debug.xcconfig │ │ ├── Flutter-Release.xcconfig │ │ └── GeneratedPluginRegistrant.swift │ ├── Runner │ │ ├── Configs │ │ │ ├── Debug.xcconfig │ │ │ ├── Release.xcconfig │ │ │ ├── Warnings.xcconfig │ │ │ └── AppInfo.xcconfig │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ ├── app_icon_16.png │ │ │ │ ├── app_icon_32.png │ │ │ │ ├── app_icon_64.png │ │ │ │ ├── app_icon_1024.png │ │ │ │ ├── app_icon_128.png │ │ │ │ ├── app_icon_256.png │ │ │ │ ├── app_icon_512.png │ │ │ │ └── Contents.json │ │ ├── AppDelegate.swift │ │ ├── Release.entitlements │ │ ├── DebugProfile.entitlements │ │ ├── MainFlutterWindow.swift │ │ └── Info.plist │ ├── .gitignore │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── web │ ├── favicon.png │ ├── icons │ │ ├── Icon-192.png │ │ ├── Icon-512.png │ │ ├── Icon-maskable-192.png │ │ └── Icon-maskable-512.png │ ├── manifest.json │ └── index.html ├── android │ ├── gradle.properties │ ├── app │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── 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 │ │ │ │ │ ├── drawable │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── drawable-v21 │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── values │ │ │ │ │ │ └── styles.xml │ │ │ │ │ └── values-night │ │ │ │ │ │ └── styles.xml │ │ │ │ ├── kotlin │ │ │ │ │ └── com │ │ │ │ │ │ └── betterment │ │ │ │ │ │ └── example │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── settings.gradle │ └── build.gradle ├── test │ ├── widgets │ │ ├── goldens │ │ │ └── ci │ │ │ │ ├── red_button.png │ │ │ │ └── contact_list_tile.png │ │ ├── contact_list_tile_golden_test.dart │ │ ├── red_button_golden_test.dart │ │ └── multiple_icons_golden_test.dart │ └── flutter_test_config.dart ├── windows │ ├── runner │ │ ├── resources │ │ │ └── app_icon.ico │ │ ├── resource.h │ │ ├── utils.h │ │ ├── runner.exe.manifest │ │ ├── flutter_window.h │ │ ├── CMakeLists.txt │ │ ├── main.cpp │ │ ├── utils.cpp │ │ ├── flutter_window.cpp │ │ ├── Runner.rc │ │ └── win32_window.h │ ├── flutter │ │ ├── generated_plugin_registrant.cc │ │ ├── generated_plugin_registrant.h │ │ ├── generated_plugins.cmake │ │ └── CMakeLists.txt │ ├── .gitignore │ └── CMakeLists.txt ├── analysis_options.yaml ├── pubspec.yaml ├── .gitignore ├── README.md ├── .metadata └── example.md ├── .github ├── ISSUE_TEMPLATE │ ├── config.yaml │ ├── feature_request.yaml │ └── bug_report.yaml ├── CODEOWNERS ├── workflows │ ├── semantic-pull-request.yaml │ ├── post_merge.yaml │ ├── update_goldens.yaml │ ├── main.yaml │ └── check_compat.yaml └── pull_request_template.md ├── test ├── helpers │ ├── helpers.dart │ └── fake_test_asset_bundle.dart ├── src │ ├── blocked_text_image_reference.png │ ├── golden_test_scenario_constraints_test.dart │ ├── blocked_text_painting_context_golden_test.dart │ ├── alchemist_test_variant_test.dart │ ├── interactions_test.dart │ ├── host_platform_test.dart │ └── golden_test_scenario_test.dart ├── smoke_tests │ ├── goldens │ │ ├── 3.32.0 │ │ │ └── ci │ │ │ │ ├── dropdown_smoke_test.png │ │ │ │ ├── asset_image_smoke_test.png │ │ │ │ ├── back_button_smoke_test.png │ │ │ │ ├── error_message_smoke_test.png │ │ │ │ ├── network_image_smoke_test.png │ │ │ │ ├── timer_button_smoke_test.png │ │ │ │ ├── unconstrained_smoke_test.png │ │ │ │ ├── constrained_big_smoke_test.png │ │ │ │ ├── render_object_text_smoke_test.png │ │ │ │ ├── composited_transform_smoke_test.png │ │ │ │ ├── interactions_smoke_test_pressed.png │ │ │ │ ├── interactions_smoke_test_regular.png │ │ │ │ └── interactions_smoke_test_long_pressed.png │ │ └── 3.35.0 │ │ │ └── ci │ │ │ ├── dropdown_smoke_test.png │ │ │ ├── asset_image_smoke_test.png │ │ │ ├── back_button_smoke_test.png │ │ │ ├── error_message_smoke_test.png │ │ │ ├── network_image_smoke_test.png │ │ │ ├── timer_button_smoke_test.png │ │ │ ├── unconstrained_smoke_test.png │ │ │ ├── constrained_big_smoke_test.png │ │ │ ├── render_object_text_smoke_test.png │ │ │ ├── composited_transform_smoke_test.png │ │ │ ├── interactions_smoke_test_pressed.png │ │ │ ├── interactions_smoke_test_regular.png │ │ │ └── interactions_smoke_test_long_pressed.png │ ├── back_button_smoke_test.dart │ ├── error_message_smoke_test.dart │ ├── asset_image_smoke_test.dart │ ├── network_image_smoke_test.dart │ ├── unconstrained_smoke_test.dart │ ├── composited_transform_smoke_test.dart │ ├── timer_button_smoke_test.dart │ ├── dropdown_smoke_test.dart │ ├── render_object_smoke_test.dart │ └── interactions_smoke_test.dart └── flutter_test_config.dart ├── .vscode ├── settings.json └── launch.json ├── assets ├── fonts │ └── Roboto │ │ ├── Roboto-Black.ttf │ │ ├── Roboto-Bold.ttf │ │ ├── Roboto-Light.ttf │ │ ├── Roboto-Thin.ttf │ │ ├── Roboto-Medium.ttf │ │ └── Roboto-Regular.ttf └── readme │ ├── ci_list_tile_golden_file.png │ └── macos_list_tile_golden_file.png ├── codecov.yml ├── analysis_options.yaml ├── dart_test.yaml ├── cspell.json ├── .metadata ├── lib ├── alchemist.dart └── src │ ├── golden_test_scenario_constraints.dart │ ├── interactions.dart │ ├── alchemist_test_variant.dart │ ├── pumps.dart │ ├── golden_test_theme.dart │ ├── host_platform.dart │ ├── golden_test_scenario.dart │ └── golden_test_runner.dart ├── tool └── verify_pub_score.sh ├── coverage_badge.svg ├── LICENSE ├── pubspec.yaml ├── .gitignore ├── CONTRIBUTING.md └── RECOMMENDED_SETUP_GUIDE.md /.fvmrc: -------------------------------------------------------------------------------- 1 | { 2 | "flutter": "3.32.8" 3 | } -------------------------------------------------------------------------------- /example/linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /example/lib/home/home.dart: -------------------------------------------------------------------------------- 1 | export 'home_page.dart'; 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yaml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /test/helpers/helpers.dart: -------------------------------------------------------------------------------- 1 | export 'fake_test_asset_bundle.dart'; 2 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dart.flutterSdkPath": ".fvm/versions/3.32.8" 3 | } -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "ephemeral/Flutter-Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "ephemeral/Flutter-Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/lib/widgets/widgets.dart: -------------------------------------------------------------------------------- 1 | export 'contact_list_tile.dart'; 2 | export 'red_button.dart'; 3 | -------------------------------------------------------------------------------- /example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/example/web/favicon.png -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /assets/fonts/Roboto/Roboto-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/assets/fonts/Roboto/Roboto-Black.ttf -------------------------------------------------------------------------------- /assets/fonts/Roboto/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/assets/fonts/Roboto/Roboto-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/Roboto/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/assets/fonts/Roboto/Roboto-Light.ttf -------------------------------------------------------------------------------- /assets/fonts/Roboto/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/assets/fonts/Roboto/Roboto-Thin.ttf -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: "100...100" 3 | status: 4 | project: 5 | default: 6 | target: 100 7 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:very_good_analysis/analysis_options.5.1.0.yaml 2 | 3 | formatter: 4 | page_width: 80 5 | -------------------------------------------------------------------------------- /assets/fonts/Roboto/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/assets/fonts/Roboto/Roboto-Medium.ttf -------------------------------------------------------------------------------- /assets/fonts/Roboto/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/assets/fonts/Roboto/Roboto-Regular.ttf -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /example/web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/example/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/example/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /test/src/blocked_text_image_reference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/src/blocked_text_image_reference.png -------------------------------------------------------------------------------- /assets/readme/ci_list_tile_golden_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/assets/readme/ci_list_tile_golden_file.png -------------------------------------------------------------------------------- /example/macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /assets/readme/macos_list_tile_golden_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/assets/readme/macos_list_tile_golden_file.png -------------------------------------------------------------------------------- /example/test/widgets/goldens/ci/red_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/example/test/widgets/goldens/ci/red_button.png -------------------------------------------------------------------------------- /example/windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/example/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /example/test/widgets/goldens/ci/contact_list_tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/example/test/widgets/goldens/ci/contact_list_tile.png -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:very_good_analysis/analysis_options.3.1.0.yaml 2 | 3 | linter: 4 | rules: 5 | public_member_api_docs: false 6 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Every request must be reviewed and accepted by: 2 | 3 | * @Kirpal @jeroen-meijer @btrautmann @jolexxa @marcossevilla @Betterment/mobile-platform 4 | -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.32.0/ci/dropdown_smoke_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.32.0/ci/dropdown_smoke_test.png -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.35.0/ci/dropdown_smoke_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.35.0/ci/dropdown_smoke_test.png -------------------------------------------------------------------------------- /dart_test.yaml: -------------------------------------------------------------------------------- 1 | # The existence of this file prevents warnings about unrecognized tags when 2 | # running Alchemist tests. 3 | 4 | tags: 5 | golden: 6 | timeout: 15s 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/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/Betterment/alchemist/HEAD/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/Betterment/alchemist/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.32.0/ci/asset_image_smoke_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.32.0/ci/asset_image_smoke_test.png -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.32.0/ci/back_button_smoke_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.32.0/ci/back_button_smoke_test.png -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.35.0/ci/asset_image_smoke_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.35.0/ci/asset_image_smoke_test.png -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.35.0/ci/back_button_smoke_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.35.0/ci/back_button_smoke_test.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/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/Betterment/alchemist/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/app/app.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | void main() { 5 | runApp(const AlchemistExampleApp()); 6 | } 7 | -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.32.0/ci/error_message_smoke_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.32.0/ci/error_message_smoke_test.png -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.32.0/ci/network_image_smoke_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.32.0/ci/network_image_smoke_test.png -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.32.0/ci/timer_button_smoke_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.32.0/ci/timer_button_smoke_test.png -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.32.0/ci/unconstrained_smoke_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.32.0/ci/unconstrained_smoke_test.png -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.35.0/ci/error_message_smoke_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.35.0/ci/error_message_smoke_test.png -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.35.0/ci/network_image_smoke_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.35.0/ci/network_image_smoke_test.png -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.35.0/ci/timer_button_smoke_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.35.0/ci/timer_button_smoke_test.png -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.35.0/ci/unconstrained_smoke_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.35.0/ci/unconstrained_smoke_test.png -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.32.0/ci/constrained_big_smoke_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.32.0/ci/constrained_big_smoke_test.png -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.35.0/ci/constrained_big_smoke_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.35.0/ci/constrained_big_smoke_test.png -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.32.0/ci/render_object_text_smoke_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.32.0/ci/render_object_text_smoke_test.png -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.35.0/ci/render_object_text_smoke_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.35.0/ci/render_object_text_smoke_test.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.32.0/ci/composited_transform_smoke_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.32.0/ci/composited_transform_smoke_test.png -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.32.0/ci/interactions_smoke_test_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.32.0/ci/interactions_smoke_test_pressed.png -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.32.0/ci/interactions_smoke_test_regular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.32.0/ci/interactions_smoke_test_regular.png -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.35.0/ci/composited_transform_smoke_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.35.0/ci/composited_transform_smoke_test.png -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.35.0/ci/interactions_smoke_test_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.35.0/ci/interactions_smoke_test_pressed.png -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.35.0/ci/interactions_smoke_test_regular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.35.0/ci/interactions_smoke_test_regular.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/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/Betterment/alchemist/HEAD/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/Betterment/alchemist/HEAD/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/Betterment/alchemist/HEAD/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/Betterment/alchemist/HEAD/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/Betterment/alchemist/HEAD/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/Betterment/alchemist/HEAD/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/Betterment/alchemist/HEAD/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/Betterment/alchemist/HEAD/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/Betterment/alchemist/HEAD/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/Betterment/alchemist/HEAD/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/Betterment/alchemist/HEAD/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/Betterment/alchemist/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.32.0/ci/interactions_smoke_test_long_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.32.0/ci/interactions_smoke_test_long_pressed.png -------------------------------------------------------------------------------- /test/smoke_tests/goldens/3.35.0/ci/interactions_smoke_test_long_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/test/smoke_tests/goldens/3.35.0/ci/interactions_smoke_test_long_pressed.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Betterment/alchemist/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/linux/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/betterment/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.betterment.alchemist-example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | 9 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 10 | } 11 | -------------------------------------------------------------------------------- /example/linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | 10 | void fl_register_plugins(FlPluginRegistry* registry) { 11 | } 12 | -------------------------------------------------------------------------------- /example/windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | 10 | void RegisterPlugins(flutter::PluginRegistry* registry) { 11 | } 12 | -------------------------------------------------------------------------------- /example/test/flutter_test_config.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; 4 | 5 | Future testExecutable(FutureOr Function() testMain) async { 6 | LeakTesting.enable(); 7 | await testMain(); 8 | } 9 | -------------------------------------------------------------------------------- /example/macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @NSApplicationMain 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "words": [ 3 | "alchemistconfig", 4 | "automaticcustom", 5 | "endtemplate", 6 | "goldens", 7 | "LTRBXY", 8 | "LTWH", 9 | "Microtask", 10 | "Occluder", 11 | "rects", 12 | "RGBO", 13 | "Roboto", 14 | "rrect" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /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-7.4-all.zip 7 | -------------------------------------------------------------------------------- /example/macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner.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/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.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: d79295af24c3ed621c33713ecda14ad196fd9c31 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /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 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /example/windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /lib/alchemist.dart: -------------------------------------------------------------------------------- 1 | export 'src/alchemist_config.dart'; 2 | export 'src/blocked_text_image.dart'; 3 | export 'src/golden_test.dart'; 4 | export 'src/golden_test_group.dart'; 5 | export 'src/golden_test_scenario.dart'; 6 | export 'src/golden_test_theme.dart'; 7 | export 'src/host_platform.dart'; 8 | export 'src/interactions.dart'; 9 | export 'src/pumps.dart'; 10 | export 'src/utilities.dart' show TestAssetBundle; 11 | -------------------------------------------------------------------------------- /example/linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /example/windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /.github/workflows/semantic-pull-request.yaml: -------------------------------------------------------------------------------- 1 | name: Semantic PR 2 | 3 | # ignore: RiskyTriggers 4 | on: [pull_request_target] 5 | 6 | jobs: 7 | validate_title: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Validate PR title 11 | uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3 12 | env: 13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 14 | -------------------------------------------------------------------------------- /test/smoke_tests/back_button_smoke_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:alchemist/src/golden_test.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | void main() { 6 | group('smoke test', () { 7 | goldenTest( 8 | 'succeeds with a BackButton widget', 9 | fileName: 'back_button_smoke_test', 10 | builder: () => const BackButton(), 11 | ); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /example/macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/lib/app/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/home/home.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class AlchemistExampleApp extends StatelessWidget { 5 | const AlchemistExampleApp({super.key}); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return MaterialApp( 10 | title: 'Flutter Demo', 11 | theme: ThemeData( 12 | primarySwatch: Colors.blue, 13 | ), 14 | home: const HomePage(), 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 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/linux/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /example/macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController.init() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Update Goldens", 6 | "request": "launch", 7 | "type": "dart", 8 | "args": [ 9 | "--update-goldens" 10 | ], 11 | "program": "${relativeFile}", 12 | "flutterMode": "debug", 13 | "console": "debugConsole", 14 | "codeLens": { 15 | "for": [ 16 | "debug-test", 17 | "debug-test-file" 18 | ] 19 | } 20 | }, 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: An example project showcasing Alchemist's testing functionality. 3 | 4 | publish_to: "none" 5 | 6 | version: 1.0.0+1 7 | 8 | environment: 9 | sdk: ">=2.17.0 <3.0.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | 15 | dev_dependencies: 16 | alchemist: 17 | path: ../ 18 | flutter_test: 19 | sdk: flutter 20 | leak_tracker_flutter_testing: any 21 | very_good_analysis: ^3.1.0 22 | 23 | flutter: 24 | uses-material-design: true 25 | -------------------------------------------------------------------------------- /example/windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | 5 | ## Type of Change 6 | 7 | 8 | 9 | - [ ] ✨ New feature (non-breaking change which adds functionality) 10 | - [ ] 🛠️ Bug fix (non-breaking change which fixes an issue) 11 | - [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change) 12 | - [ ] 🧹 Code refactor 13 | - [ ] ✅ Build configuration change 14 | - [ ] 📝 Documentation 15 | - [ ] 🗑️ Chore 16 | -------------------------------------------------------------------------------- /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/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/smoke_tests/error_message_smoke_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:alchemist/src/golden_test.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | void main() { 6 | group('smoke test', () { 7 | goldenTest( 8 | 'succeeds with an error message', 9 | fileName: 'error_message_smoke_test', 10 | builder: () => SizedBox( 11 | width: 200, 12 | height: 200, 13 | child: ErrorWidget(FlutterError('This is an error message.')), 14 | ), 15 | ); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /test/smoke_tests/asset_image_smoke_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:alchemist/src/golden_test.dart'; 2 | import 'package:alchemist/src/pumps.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | import '../helpers/helpers.dart'; 7 | 8 | void main() { 9 | group('smoke test', () { 10 | goldenTest( 11 | 'succeeds with an asset image', 12 | fileName: 'asset_image_smoke_test', 13 | pumpBeforeTest: precacheImages, 14 | builder: () => DefaultAssetBundle( 15 | bundle: FakeTestAssetBundle(), 16 | child: Image.asset('test.png'), 17 | ), 18 | ); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = example 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.betterment.alchemist-example 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2022 com.betterment. All rights reserved. 15 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /test/smoke_tests/network_image_smoke_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:alchemist/alchemist.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:mocktail_image_network/mocktail_image_network.dart'; 5 | 6 | void main() { 7 | group('smoke test', () { 8 | goldenTest( 9 | 'succeeds with a network image', 10 | fileName: 'network_image_smoke_test', 11 | pumpWidget: (tester, widget) async { 12 | await mockNetworkImages(() => tester.pumpWidget(widget)); 13 | }, 14 | builder: () => Padding( 15 | padding: const EdgeInsets.all(8), 16 | child: Image.network('https://fakeurl.com/image.png'), 17 | ), 18 | ); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.1.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /example/windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /test/helpers/fake_test_asset_bundle.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:alchemist/alchemist.dart'; 5 | 6 | final redPixelImage = base64Decode( 7 | 'iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAApElEQVR42u3RAQ0AAAjD' 8 | 'MO5fNCCDkC5z0HTVrisFCBABASIgQAQEiIAAAQJEQIAICBABASIgQAREQIAICBABASIg' 9 | 'QAREQIAICBABASIgQAREQIAICBABASIgQAREQIAICBABASIgQAREQIAICBABASIgQARE' 10 | 'QIAICBABASIgQAREQIAICBABASIgQAREQIAICBABASIgQAQECBAgAgJEQIAIyPcGFY7H' 11 | 'nV2aPXoAAAAASUVORK5CYII=', 12 | ); 13 | 14 | class FakeTestAssetBundle extends TestAssetBundle { 15 | @override 16 | Future load(String key) async { 17 | if (key.endsWith('png')) { 18 | return ByteData.view(redPixelImage.buffer); 19 | } 20 | return super.load(key); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /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 17 | -------------------------------------------------------------------------------- /test/smoke_tests/unconstrained_smoke_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:alchemist/src/golden_test.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | void main() { 6 | group('smoke test', () { 7 | goldenTest( 8 | 'succeeds with an unconstrained widget', 9 | fileName: 'unconstrained_smoke_test', 10 | constraints: const BoxConstraints(maxWidth: 3000, maxHeight: 3000), 11 | builder: () => 12 | const SizedBox.expand(child: ColoredBox(color: Colors.red)), 13 | ); 14 | 15 | goldenTest( 16 | 'succeeds with a big constrained widget', 17 | fileName: 'constrained_big_smoke_test', 18 | builder: () => const SizedBox( 19 | width: 3000, 20 | height: 3000, 21 | child: ColoredBox(color: Colors.red), 22 | ), 23 | ); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /example/linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | ) 7 | 8 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 9 | ) 10 | 11 | set(PLUGIN_BUNDLED_LIBRARIES) 12 | 13 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 14 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 15 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 16 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 18 | endforeach(plugin) 19 | 20 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 21 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 22 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 23 | endforeach(ffi_plugin) 24 | -------------------------------------------------------------------------------- /example/windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | ) 7 | 8 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 9 | ) 10 | 11 | set(PLUGIN_BUNDLED_LIBRARIES) 12 | 13 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 14 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 15 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 16 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 18 | endforeach(plugin) 19 | 20 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 21 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 22 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 23 | endforeach(ffi_plugin) 24 | -------------------------------------------------------------------------------- /test/smoke_tests/composited_transform_smoke_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:alchemist/src/golden_test.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | class _SmokeTest extends StatelessWidget { 6 | _SmokeTest({super.key}) : _link = LayerLink(); 7 | 8 | final LayerLink _link; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Stack( 13 | children: [ 14 | CompositedTransformTarget(link: _link, child: const FlutterLogo()), 15 | CompositedTransformFollower(link: _link, child: const Text('label')), 16 | ], 17 | ); 18 | } 19 | } 20 | 21 | void main() { 22 | group('smoke test', () { 23 | goldenTest( 24 | 'succeeds with CompositedTransformFollower', 25 | fileName: 'composited_transform_smoke_test', 26 | builder: _SmokeTest.new, 27 | ); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /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 | 9.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/lib/widgets/red_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class RedButton extends StatelessWidget { 4 | const RedButton({ 5 | super.key, 6 | required this.onPressed, 7 | this.icon, 8 | required this.child, 9 | }); 10 | 11 | final VoidCallback? onPressed; 12 | final Widget? icon; 13 | final Widget child; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | final style = ElevatedButton.styleFrom( 18 | backgroundColor: Colors.red, 19 | foregroundColor: Colors.white, 20 | ); 21 | if (icon != null) { 22 | return ElevatedButton.icon( 23 | style: style, 24 | onPressed: onPressed, 25 | icon: icon, 26 | label: child, 27 | ); 28 | } else { 29 | return ElevatedButton( 30 | style: style, 31 | onPressed: onPressed, 32 | child: child, 33 | ); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/smoke_tests/timer_button_smoke_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:alchemist/src/golden_test.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | class TimerButton extends StatelessWidget { 8 | const TimerButton({super.key}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return ElevatedButton( 13 | child: const Text('Button'), 14 | onPressed: () { 15 | Timer(Duration.zero, () {}); 16 | }, 17 | ); 18 | } 19 | } 20 | 21 | void main() { 22 | group('smoke test', () { 23 | goldenTest( 24 | 'succeeds after tapping button with timer', 25 | fileName: 'timer_button_smoke_test', 26 | pumpBeforeTest: (tester) async { 27 | await tester.tap(find.byType(TimerButton)); 28 | await tester.pumpAndSettle(); 29 | }, 30 | builder: () => const TimerButton(), 31 | ); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /example/windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Web related 36 | lib/generated_plugin_registrant.dart 37 | 38 | # Symbolication related 39 | app.*.symbols 40 | 41 | # Obfuscation related 42 | app.*.map.json 43 | 44 | # Android Studio will place build artifacts here 45 | /android/app/debug 46 | /android/app/profile 47 | /android/app/release 48 | 49 | # Ignore platform-specific goldens 50 | **/goldens/macos 51 | **/goldens/linux 52 | **/goldens/windows 53 | -------------------------------------------------------------------------------- /example/windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /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 | 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Betterment LLC 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, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/src/golden_test_scenario_constraints.dart: -------------------------------------------------------------------------------- 1 | import 'package:alchemist/src/golden_test_scenario.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | /// {@template golden_test_scenario_constraints} 5 | /// Applies constraints to the children of [GoldenTestScenario] widgets. This is 6 | /// intended for internal use only. 7 | /// {@endtemplate} 8 | class GoldenTestScenarioConstraints extends InheritedWidget { 9 | /// {@macro golden_test_scenario_constraints} 10 | const GoldenTestScenarioConstraints({ 11 | required super.child, 12 | required this.constraints, 13 | super.key, 14 | }); 15 | 16 | /// The constraints to apply to the scenario's child. 17 | final BoxConstraints? constraints; 18 | 19 | @override 20 | bool updateShouldNotify(covariant GoldenTestScenarioConstraints oldWidget) { 21 | return oldWidget.constraints != constraints; 22 | } 23 | 24 | /// Obtains the constraints from the nearest instance of this widget from the 25 | /// given context. 26 | static BoxConstraints? maybeOf(BuildContext context) => context 27 | .dependOnInheritedWidgetOfExactType() 28 | ?.constraints; 29 | } 30 | -------------------------------------------------------------------------------- /example/macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /test/smoke_tests/dropdown_smoke_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:alchemist/src/golden_test.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | class TestDropdownButton extends StatelessWidget { 6 | const TestDropdownButton({super.key}); 7 | 8 | @override 9 | Widget build(BuildContext context) => Center( 10 | child: DropdownButton( 11 | value: '0', 12 | items: const [ 13 | DropdownMenuItem(value: '0', child: Text('0')), 14 | DropdownMenuItem(value: '1', child: Text('1')), 15 | DropdownMenuItem(value: '2', child: Text('2')), 16 | ], 17 | onChanged: (_) {}, 18 | ), 19 | ); 20 | } 21 | 22 | void main() { 23 | group('smoke test', () { 24 | goldenTest( 25 | 'succeeds after tapping dropdown', 26 | fileName: 'dropdown_smoke_test', 27 | constraints: const BoxConstraints(maxWidth: 200, maxHeight: 250), 28 | pumpBeforeTest: (tester) async { 29 | await tester.pumpAndSettle(); 30 | await tester.tap(find.byType(DropdownButton)); 31 | await tester.pumpAndSettle(); 32 | }, 33 | builder: () { 34 | return const TestDropdownButton(); 35 | }, 36 | ); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/post_merge.yaml: -------------------------------------------------------------------------------- 1 | name: post-merge 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | 8 | jobs: 9 | upload_coverage: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 14 | 15 | - name: Determine Flutter version 16 | id: flutter_version 17 | run: | 18 | # Grab the Flutter version from the .fvmrc file 19 | FLUTTER_VERSION=$(grep '"flutter"' .fvmrc | grep -o '[0-9][0-9.]*') 20 | echo "flutter_version=$FLUTTER_VERSION" >> "$GITHUB_OUTPUT" 21 | 22 | - uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0 23 | with: 24 | flutter-version: ${{ steps.flutter_version.outputs.flutter_version }} 25 | cache: true 26 | 27 | - name: Install Dependencies 28 | run: flutter packages get 29 | 30 | - name: Disable animations 31 | run: flutter config --no-cli-animations 32 | 33 | - name: Run tests 34 | run: | 35 | flutter test --no-pub --coverage --test-randomize-ordering-seed=random 36 | 37 | - uses: codecov/codecov-action@c2fcb216de2b0348de0100baa3ea2cad9f100a01 # v5.1.0 38 | with: 39 | files: coverage/lcov.info 40 | -------------------------------------------------------------------------------- /example/test/widgets/contact_list_tile_golden_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:alchemist/alchemist.dart'; 2 | import 'package:example/widgets/widgets.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | void main() { 6 | group('Contact List Tile Golden Tests', () { 7 | goldenTest( 8 | 'renders correctly', 9 | fileName: 'contact_list_tile', 10 | builder: () => GoldenTestGroup( 11 | children: [ 12 | GoldenTestScenario( 13 | name: 'enabled, one name', 14 | child: ContactListTile( 15 | onPressed: () {}, 16 | name: 'Contact', 17 | email: 'contact@example.com', 18 | ), 19 | ), 20 | GoldenTestScenario( 21 | name: 'enabled, two names', 22 | child: ContactListTile( 23 | onPressed: () {}, 24 | name: 'Contact List', 25 | email: 'contactlist@example.com', 26 | ), 27 | ), 28 | GoldenTestScenario( 29 | name: 'enabled, three names', 30 | child: ContactListTile( 31 | onPressed: () {}, 32 | name: 'Contact List Tile', 33 | email: 'contactlisttile@example.com', 34 | ), 35 | ), 36 | ], 37 | ), 38 | ); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: alchemist 2 | description: A support package that aims to make golden testing in Flutter 3 | easier and more streamlined. 4 | version: 0.13.0 5 | homepage: https://github.com/Betterment/alchemist 6 | repository: https://github.com/Betterment/alchemist 7 | 8 | environment: 9 | sdk: ">=3.8.0 <4.0.0" 10 | flutter: ">=3.32.0" 11 | 12 | dependencies: 13 | equatable: ^2.0.3 14 | flutter: 15 | sdk: flutter 16 | flutter_test: 17 | sdk: flutter 18 | meta: ^1.7.0 19 | 20 | dev_dependencies: 21 | mocktail: ^0.3.0 22 | mocktail_image_network: ^0.3.1 23 | path: ^1.8.3 24 | test: ^1.21.1 25 | version: ^3.0.2 26 | very_good_analysis: ^5.1.0 27 | 28 | flutter: 29 | uses-material-design: true 30 | # Used for readable platform golden tests. 31 | fonts: 32 | - family: Roboto 33 | fonts: 34 | - asset: assets/fonts/Roboto/Roboto-Thin.ttf 35 | weight: 100 36 | - asset: assets/fonts/Roboto/Roboto-Light.ttf 37 | weight: 300 38 | - asset: assets/fonts/Roboto/Roboto-Regular.ttf 39 | weight: 400 40 | - asset: assets/fonts/Roboto/Roboto-Medium.ttf 41 | weight: 500 42 | - asset: assets/fonts/Roboto/Roboto-Bold.ttf 43 | weight: 700 44 | - asset: assets/fonts/Roboto/Roboto-Black.ttf 45 | weight: 900 46 | -------------------------------------------------------------------------------- /example/windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Disable Windows macros that collide with C++ standard library functions. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 25 | 26 | # Add dependency libraries and include directories. Add any application-specific 27 | # dependencies here. 28 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 29 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 30 | 31 | # Run the Flutter tool portions of the build. This must not be removed. 32 | add_dependencies(${BINARY_NAME} flutter_assemble) 33 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Alchemist Example Project 2 | 3 | > For a quick explanation on how to get started with Alchemist, read the [example.md][example_markdown] file or the [Recommended Setup Guide][setup_guide]. 4 | 5 | This is an example app that showcases the Alchemist golden testing package. 6 | 7 | It contains two custom widgets that each have their own golden tests; a [RedButton](./lib/widgets/red_button.dart) and a [ContactListTile](./lib/widgets/contact_list_tile.dart). Both have several variations, all of which are showcased in the app. 8 | 9 | The primary goal of this app is to show how to use the Alchemist golden testing package to test widgets. Please see Alchemist's [README](../README.md) for more information, and visit the [`test/`](./test/) directory in this app to see how these widgets are tested. 10 | 11 | ## Getting Started 12 | 13 | This project is a starting point for a Flutter application. 14 | 15 | A few resources to get you started if this is your first Flutter project: 16 | 17 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 18 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 19 | 20 | For help getting started with Flutter development, view the 21 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 22 | samples, guidance on mobile development, and a full API reference. 23 | 24 | [example_markdown]: ./example.md 25 | [setup_guide]: ../RECOMMENDED_SETUP_GUIDE.md 26 | -------------------------------------------------------------------------------- /example/windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(1280, 720); 30 | if (!window.CreateAndShow(L"example", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/interactions.dart: -------------------------------------------------------------------------------- 1 | import 'package:alchemist/src/utilities.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/gestures.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | /// An interaction to perform while rendering a golden test. Returns 7 | /// an asynchronous callback that should be called to cleanup when 8 | /// the golden test completes. 9 | typedef Interaction = Future Function(WidgetTester); 10 | 11 | /// Presses all widgets matching `finder`. 12 | Interaction press( 13 | Finder finder, { 14 | Duration? holdFor = const Duration(milliseconds: 300), 15 | }) => (WidgetTester tester) async { 16 | final gestures = await tester.pressAll(finder); 17 | await tester.pump(kPressTimeout); 18 | await tester.pump(holdFor); 19 | return gestures.releaseAll; 20 | }; 21 | 22 | /// Long-presses all widgets matching [finder]. 23 | Interaction longPress(Finder finder) => (WidgetTester tester) async { 24 | final gestures = await tester.pressAll(finder); 25 | await tester.pump(kLongPressTimeout); 26 | return gestures.releaseAll; 27 | }; 28 | 29 | /// Scrolls all widgets matching `finder`. 30 | Interaction scroll( 31 | Finder finder, { 32 | required Offset offset, 33 | double speed = kMinFlingVelocity, 34 | }) => (WidgetTester tester) async { 35 | final elements = finder.evaluate(); 36 | for (final element in elements) { 37 | await tester.fling(find.byWidget(element.widget), offset, speed); 38 | } 39 | return; 40 | }; 41 | -------------------------------------------------------------------------------- /example/test/widgets/red_button_golden_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:alchemist/alchemist.dart'; 2 | import 'package:example/widgets/widgets.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | void main() { 7 | group('Red Button Golden Tests', () { 8 | goldenTest( 9 | 'renders correctly', 10 | fileName: 'red_button', 11 | builder: () => GoldenTestGroup( 12 | children: [ 13 | GoldenTestScenario( 14 | name: 'enabled', 15 | child: RedButton( 16 | onPressed: () {}, 17 | child: const Text('Red Button'), 18 | ), 19 | ), 20 | GoldenTestScenario( 21 | name: 'disabled', 22 | child: const RedButton( 23 | onPressed: null, 24 | child: Text('Red Button'), 25 | ), 26 | ), 27 | GoldenTestScenario( 28 | name: 'with icon', 29 | child: RedButton( 30 | onPressed: () {}, 31 | icon: const Icon(Icons.add), 32 | child: const Text('Red Button'), 33 | ), 34 | ), 35 | GoldenTestScenario( 36 | name: 'disabled with icon', 37 | child: const RedButton( 38 | onPressed: null, 39 | icon: Icon(Icons.add), 40 | child: Text('Red Button'), 41 | ), 42 | ), 43 | ], 44 | ), 45 | ); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/smoke_tests/render_object_smoke_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:alchemist/alchemist.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | void main() { 6 | group('smoke test', () { 7 | goldenTest( 8 | 'succeeds for custom render object drawing text', 9 | fileName: 'render_object_text_smoke_test', 10 | builder: () => const SizedBox( 11 | height: 400, 12 | width: 400, 13 | child: _CustomExampleRenderObject(), 14 | ), 15 | ); 16 | }); 17 | } 18 | 19 | class _CustomExampleRenderObject extends LeafRenderObjectWidget { 20 | const _CustomExampleRenderObject(); 21 | 22 | @override 23 | _CustomExampleRenderBox createRenderObject(BuildContext context) { 24 | return _CustomExampleRenderBox(); 25 | } 26 | } 27 | 28 | class _CustomExampleRenderBox extends RenderBox { 29 | @override 30 | void paint(PaintingContext context, Offset offset) { 31 | final textPainter = TextPainter( 32 | text: const TextSpan( 33 | text: 'text', 34 | style: TextStyle( 35 | fontFamily: 'Roboto', 36 | color: Colors.black, 37 | fontSize: 24, 38 | ), 39 | ), 40 | textDirection: TextDirection.ltr, 41 | )..layout(); 42 | textPainter.paint( 43 | context.canvas, 44 | const Offset(200, 200) - textPainter.size.center(Offset.zero), 45 | ); 46 | } 47 | 48 | @override 49 | void performLayout() { 50 | size = computeDryLayout(constraints); 51 | } 52 | 53 | @override 54 | Size computeDryLayout(BoxConstraints constraints) { 55 | return constraints.constrain(Size(200, constraints.maxHeight)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Request a new feature. 3 | title: "request: " 4 | labels: ["feature request", "triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | 11 | - type: checkboxes 12 | attributes: 13 | label: Is there an existing feature request for this? 14 | description: | 15 | Please search to see if an issue already exists for the feature you would like to see. 16 | options: 17 | - label: I have searched the existing issues. 18 | required: true 19 | 20 | - type: input 21 | validations: 22 | required: true 23 | id: command 24 | attributes: 25 | label: Command 26 | description: | 27 | What feature would you like to see? 28 | placeholder: "I would love if we could do ..." 29 | 30 | - type: textarea 31 | validations: 32 | required: true 33 | id: description 34 | attributes: 35 | label: Description 36 | description: | 37 | Give us a clear and concise description of what the feature is and what it would do. 38 | placeholder: As a developer, I would like to be able to ... 39 | 40 | - type: textarea 41 | validations: 42 | required: true 43 | id: reasoning 44 | attributes: 45 | label: Reasoning 46 | description: | 47 | What is the reason for your request? 48 | Why do you think this feature would be useful? 49 | placeholder: | 50 | I think this feature would be useful because... 51 | 52 | - type: textarea 53 | id: comments 54 | attributes: 55 | label: Additional context and comments 56 | description: | 57 | Anything else you want to say? 58 | -------------------------------------------------------------------------------- /example/lib/widgets/contact_list_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ContactListTile extends StatelessWidget { 4 | const ContactListTile({ 5 | super.key, 6 | required this.onPressed, 7 | required this.name, 8 | required this.email, 9 | }) : assert(name.length > 0, 'name must be non-empty'), 10 | assert(email.length > 0, 'email must be non-empty'); 11 | 12 | final VoidCallback? onPressed; 13 | final String name; 14 | final String email; 15 | 16 | bool get _isEnabled => onPressed != null; 17 | 18 | /// Combines the first character of the [name]'s first and last names. 19 | String get _initials { 20 | final charsByPart = name.split(' ').map((part) => part.split('')).toList(); 21 | final initialsBuffer = StringBuffer()..write(charsByPart[0][0]); 22 | if (charsByPart.length > 1) { 23 | initialsBuffer.write(charsByPart.last[0]); 24 | } 25 | return initialsBuffer.toString().toUpperCase(); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return Padding( 31 | padding: const EdgeInsets.symmetric(horizontal: 8), 32 | child: InkWell( 33 | onTap: onPressed, 34 | customBorder: const RoundedRectangleBorder( 35 | borderRadius: BorderRadius.all(Radius.circular(8)), 36 | ), 37 | child: ListTile( 38 | contentPadding: const EdgeInsets.symmetric(horizontal: 8), 39 | enabled: _isEnabled, 40 | leading: CircleAvatar( 41 | backgroundColor: _isEnabled ? Colors.blue.shade900 : Colors.grey, 42 | foregroundColor: Colors.white, 43 | child: Text(_initials), 44 | ), 45 | title: Text(name), 46 | subtitle: Text(email), 47 | ), 48 | ), 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /test/smoke_tests/interactions_smoke_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:alchemist/alchemist.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | void main() { 6 | group('smoke test', () { 7 | GoldenTestGroup buildSmokeTestGroup() { 8 | return GoldenTestGroup( 9 | children: [ 10 | GoldenTestScenario(name: 'scenario_text', child: const Text('text')), 11 | GoldenTestScenario( 12 | name: 'scenario_button', 13 | child: TextButton( 14 | style: const ButtonStyle( 15 | backgroundColor: WidgetStatePropertyAll(Color(0xFF2196F3)), 16 | foregroundColor: WidgetStatePropertyAll(Color(0xFFFFFFFF)), 17 | shadowColor: WidgetStatePropertyAll(Color(0xFFFF0000)), 18 | surfaceTintColor: WidgetStatePropertyAll(Color(0xFF00FF00)), 19 | overlayColor: WidgetStatePropertyAll(Color(0xFF0000FF)), 20 | ), 21 | onPressed: () {}, 22 | onLongPress: () {}, 23 | child: const Text('button'), 24 | ), 25 | ), 26 | ], 27 | ); 28 | } 29 | 30 | goldenTest( 31 | 'succeeds in regular state', 32 | fileName: 'interactions_smoke_test_regular', 33 | builder: buildSmokeTestGroup, 34 | ); 35 | 36 | goldenTest( 37 | 'succeeds while pressed', 38 | fileName: 'interactions_smoke_test_pressed', 39 | whilePerforming: press(find.byType(TextButton)), 40 | builder: buildSmokeTestGroup, 41 | ); 42 | 43 | goldenTest( 44 | 'succeeds while long pressed', 45 | fileName: 'interactions_smoke_test_long_pressed', 46 | whilePerforming: longPress(find.byType(TextButton)), 47 | builder: buildSmokeTestGroup, 48 | ); 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /lib/src/alchemist_test_variant.dart: -------------------------------------------------------------------------------- 1 | import 'package:alchemist/src/alchemist_config.dart'; 2 | import 'package:alchemist/src/host_platform.dart'; 3 | import 'package:flutter/widgets.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | /// {@template alchemist_test_variant} 7 | /// A [TestVariant] used to run both CI and platform golden tests with one 8 | /// [testWidgets] function. 9 | /// {@endtemplate} 10 | @visibleForTesting 11 | class AlchemistTestVariant extends TestVariant { 12 | /// {@macro alchemist_test_variant} 13 | AlchemistTestVariant({ 14 | required AlchemistConfig config, 15 | required HostPlatform currentPlatform, 16 | }) : _config = config, 17 | _currentPlatform = currentPlatform; 18 | 19 | final AlchemistConfig _config; 20 | final HostPlatform _currentPlatform; 21 | 22 | /// The [GoldensConfig] to use for the current variant 23 | GoldensConfig get currentConfig => _currentConfig; 24 | late GoldensConfig _currentConfig; 25 | 26 | @override 27 | String describeValue(GoldensConfig value) => value.environmentName; 28 | 29 | @override 30 | Future setUp(GoldensConfig value) async { 31 | _currentConfig = value; 32 | } 33 | 34 | @override 35 | Future tearDown( 36 | GoldensConfig value, 37 | covariant AlchemistTestVariant? memento, 38 | ) async { 39 | imageCache.clear(); 40 | } 41 | 42 | @override 43 | Iterable get values { 44 | final platformConfig = _config.platformGoldensConfig; 45 | final runPlatformTest = 46 | platformConfig.enabled && 47 | platformConfig.platforms.contains(_currentPlatform); 48 | 49 | final ciConfig = _config.ciGoldensConfig; 50 | final runCiTest = ciConfig.enabled; 51 | 52 | return {if (runPlatformTest) platformConfig, if (runCiTest) ciConfig}; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /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. 5 | 6 | version: 7 | revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 17 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 18 | - platform: android 19 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 20 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 21 | - platform: ios 22 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 23 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 24 | - platform: linux 25 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 26 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 27 | - platform: macos 28 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 29 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 30 | - platform: web 31 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 32 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 33 | - platform: windows 34 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 35 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | Example 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | example 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | $(FLUTTER_BUILD_NUMBER) 27 | LSRequiresIPhoneOS 28 | 29 | UILaunchStoryboardName 30 | LaunchScreen 31 | UIMainStoryboardFile 32 | Main 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | UIViewControllerBasedStatusBarAppearance 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /example/windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr); 51 | std::string utf8_string; 52 | if (target_length == 0 || target_length > utf8_string.max_size()) { 53 | return utf8_string; 54 | } 55 | utf8_string.resize(target_length); 56 | int converted_length = ::WideCharToMultiByte( 57 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 58 | -1, utf8_string.data(), 59 | target_length, nullptr, nullptr); 60 | if (converted_length == 0) { 61 | return std::string(); 62 | } 63 | return utf8_string; 64 | } 65 | -------------------------------------------------------------------------------- /example/windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | return true; 30 | } 31 | 32 | void FlutterWindow::OnDestroy() { 33 | if (flutter_controller_) { 34 | flutter_controller_ = nullptr; 35 | } 36 | 37 | Win32Window::OnDestroy(); 38 | } 39 | 40 | LRESULT 41 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 42 | WPARAM const wparam, 43 | LPARAM const lparam) noexcept { 44 | // Give Flutter, including plugins, an opportunity to handle window messages. 45 | if (flutter_controller_) { 46 | std::optional result = 47 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 48 | lparam); 49 | if (result) { 50 | return *result; 51 | } 52 | } 53 | 54 | switch (message) { 55 | case WM_FONTCHANGE: 56 | flutter_controller_->engine()->ReloadSystemFonts(); 57 | break; 58 | } 59 | 60 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 61 | } 62 | -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | example 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Alchemist 2 | 3 | **/failures/**/*.png 4 | 5 | # Miscellaneous 6 | *.class 7 | *.log 8 | *.pyc 9 | *.swp 10 | .DS_Store 11 | .atom/ 12 | .buildlog/ 13 | .history 14 | .svn/ 15 | 16 | # IntelliJ related 17 | *.iml 18 | *.ipr 19 | *.iws 20 | .idea/ 21 | 22 | # The .vscode folder contains launch configuration and tasks you configure in 23 | # VS Code which you may wish to be included in version control, so this line 24 | # is commented out by default. 25 | #.vscode/ 26 | 27 | # Flutter/Dart/Pub related 28 | **/doc/api/ 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .packages 33 | .pub-cache/ 34 | .pub/ 35 | build/ 36 | coverage/ 37 | pubspec.lock 38 | config/ 39 | 40 | # Android related 41 | **/android/**/gradle-wrapper.jar 42 | **/android/.gradle 43 | **/android/captures/ 44 | **/android/gradlew 45 | **/android/gradlew.bat 46 | **/android/local.properties 47 | **/android/**/GeneratedPluginRegistrant.java 48 | 49 | # iOS/XCode related 50 | **/ios/**/*.mode1v3 51 | **/ios/**/*.mode2v3 52 | **/ios/**/*.moved-aside 53 | **/ios/**/*.pbxuser 54 | **/ios/**/*.perspectivev3 55 | **/ios/**/*sync/ 56 | **/ios/**/.sconsign.dblite 57 | **/ios/**/.tags* 58 | **/ios/**/.vagrant/ 59 | **/ios/**/DerivedData/ 60 | **/ios/**/Icon? 61 | **/ios/**/Pods/ 62 | **/ios/**/.symlinks/ 63 | **/ios/**/profile 64 | **/ios/**/xcuserdata 65 | **/ios/.generated/ 66 | **/ios/Flutter/App.framework 67 | **/ios/Flutter/Flutter.framework 68 | **/ios/Flutter/Flutter.podspec 69 | **/ios/Flutter/Generated.xcconfig 70 | **/ios/Flutter/ephemeral 71 | **/ios/Flutter/app.flx 72 | **/ios/Flutter/app.zip 73 | **/ios/Flutter/flutter_assets/ 74 | **/ios/Flutter/flutter_export_environment.sh 75 | **/ios/ServiceDefinitions.json 76 | **/ios/Runner/GeneratedPluginRegistrant.* 77 | 78 | # Exceptions to above rules. 79 | !**/ios/**/default.mode1v3 80 | !**/ios/**/default.mode2v3 81 | !**/ios/**/default.pbxuser 82 | !**/ios/**/default.perspectivev3 83 | 84 | # Ignore platform-specific goldens 85 | **/goldens/macos 86 | **/goldens/linux 87 | **/goldens/windows 88 | 89 | # FVM Version Cache 90 | .fvm/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Alchemist 2 | 3 | Thanks for checking out `alchemist`! Your contributions are greatly appreciated 🎉. 4 | The following guidelines should get you started out on your path towards contribution. 5 | 6 | ## Creating a Bug Report 7 | 8 | If you've found a bug, [create an issue using the bug report template][bug_report_template] rather than immediately opening a pull request. This allows us to triage the issue as necessary and discuss potential solutions. Please try to provide as much information as possible, including detailed reproduction steps. Once one of the package maintainers has reviewed the issue and an agreement is reached regarding the fix, a pull request can be created. 9 | 10 | ## Creating a Feature Request 11 | 12 | Use the built-in [Feature Request template][feature_request_template] to add in any relevant details with your request. Once one of the package maintainers has reviewed the issue and triaged it, a pull request can be created. 13 | 14 | ## Creating a Pull Request 15 | 16 | Before creating a pull request please: 17 | 18 | 1. Fork the repository and create your branch from `main`. 19 | 2. Install all dependencies (`flutter pub get`). 20 | 3. Make your changes. 21 | 4. Add tests — only PR's with 100% test coverage are accepted! 22 | 5. Ensure the existing test suite passes locally. 23 | 6. Format your code (`dart format .`). 24 | 7. Analyze your code (`dart analyze --fatal-infos --fatal-warnings .`). 25 | 8. Create the Pull Request with [semantic title](https://github.com/zeke/semantic-pull-requests). 26 | 9. Verify that all status checks are passing. 27 | 28 | If you are fixing an issue in Alchemist, add a smoke test in `test/smoke_tests` that tests a widget which replicates the issue you are fixing to prevent future regressions. 29 | 30 | ## License 31 | 32 | This packages uses the [MIT license](https://github.com/Betterment/alchemist/blob/main/LICENSE) 33 | 34 | [bug_report_template]: https://github.com/Betterment/alchemist/issues/new?assignees=&labels=&template=bug_report.yaml 35 | [feature_request_template]: https://github.com/Betterment/alchemist/issues/new?assignees=&labels=&template=feature_request.yaml 36 | -------------------------------------------------------------------------------- /test/src/golden_test_scenario_constraints_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:alchemist/src/golden_test_scenario_constraints.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | void main() { 6 | group('GoldenTestScenarioConstraints', () { 7 | const child = SizedBox(); 8 | group('updateShouldNotify', () { 9 | test('returns false when constraints are the same', () { 10 | const newWidget = GoldenTestScenarioConstraints( 11 | constraints: BoxConstraints(maxHeight: 400), 12 | child: child, 13 | ); 14 | const oldWidget = GoldenTestScenarioConstraints( 15 | constraints: BoxConstraints(maxHeight: 400), 16 | child: child, 17 | ); 18 | 19 | expect(newWidget.updateShouldNotify(oldWidget), isFalse); 20 | }); 21 | 22 | test('returns true when constraints are different', () { 23 | const newWidget = GoldenTestScenarioConstraints( 24 | constraints: BoxConstraints(maxHeight: 400), 25 | child: child, 26 | ); 27 | const oldWidget = GoldenTestScenarioConstraints( 28 | constraints: BoxConstraints(maxWidth: 400), 29 | child: child, 30 | ); 31 | 32 | expect(newWidget.updateShouldNotify(oldWidget), isTrue); 33 | }); 34 | }); 35 | 36 | group('maybeOf', () { 37 | testWidgets('returns the constraints from the nearest widget', ( 38 | tester, 39 | ) async { 40 | late BoxConstraints? constraints; 41 | await tester.pumpWidget( 42 | GoldenTestScenarioConstraints( 43 | constraints: const BoxConstraints(maxHeight: 200), 44 | child: GoldenTestScenarioConstraints( 45 | constraints: const BoxConstraints(minWidth: 200), 46 | child: Builder( 47 | builder: (context) { 48 | constraints = GoldenTestScenarioConstraints.maybeOf(context); 49 | return const SizedBox(); 50 | }, 51 | ), 52 | ), 53 | ), 54 | ); 55 | 56 | expect(constraints, equals(const BoxConstraints(minWidth: 200))); 57 | }); 58 | }); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /.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", "triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | 11 | - type: checkboxes 12 | attributes: 13 | label: Is there an existing issue for this? 14 | description: | 15 | Please search to see if an issue already exists for the bug you encountered. 16 | options: 17 | - label: I have searched the existing issues. 18 | required: true 19 | 20 | - type: input 21 | id: version 22 | validations: 23 | required: true 24 | attributes: 25 | label: Version 26 | description: | 27 | What version are you running? 28 | placeholder: "1.2.0" 29 | 30 | - type: textarea 31 | id: description 32 | validations: 33 | required: true 34 | attributes: 35 | label: Description 36 | description: | 37 | Give us a clear and concise description of what the bug is and what happened. 38 | placeholder: It throws an error if I ... 39 | 40 | - type: textarea 41 | id: reproduction 42 | validations: 43 | required: true 44 | attributes: 45 | label: Steps to reproduce 46 | description: | 47 | What steps can we take to reproduce the bug? 48 | placeholder: | 49 | 1. When golden tests are setup with ... 50 | 2. It does [this] instead of [that] ... 51 | 3. I think it should do [that] because of [this]. 52 | 53 | - type: textarea 54 | id: expected 55 | validations: 56 | required: true 57 | attributes: 58 | label: Expected behavior 59 | description: | 60 | What did you expect to happen? 61 | placeholder: | 62 | When running ..., it should ... 63 | 64 | - type: textarea 65 | id: screenshots 66 | validations: 67 | required: false 68 | attributes: 69 | label: Screenshots 70 | description: | 71 | If you have any screenshots, please attach them here. 72 | 73 | - type: textarea 74 | id: comments 75 | attributes: 76 | label: Additional context and comments 77 | description: | 78 | Anything else you want to say? 79 | -------------------------------------------------------------------------------- /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 flutter.compileSdkVersion 30 | ndkVersion flutter.ndkVersion 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = '1.8' 39 | } 40 | 41 | sourceSets { 42 | main.java.srcDirs += 'src/main/kotlin' 43 | } 44 | 45 | defaultConfig { 46 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 47 | applicationId "com.betterment.alchemist-example" 48 | // You can update the following values to match your application needs. 49 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. 50 | minSdkVersion flutter.minSdkVersion 51 | targetSdkVersion flutter.targetSdkVersion 52 | versionCode flutterVersionCode.toInteger() 53 | versionName flutterVersionName 54 | } 55 | 56 | buildTypes { 57 | release { 58 | // TODO: Add your own signing config for the release build. 59 | // Signing with the debug keys for now, so `flutter run --release` works. 60 | signingConfig signingConfigs.debug 61 | } 62 | } 63 | } 64 | 65 | flutter { 66 | source '../..' 67 | } 68 | 69 | dependencies { 70 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 71 | } 72 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/src/blocked_text_painting_context_golden_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui' as ui; 2 | 3 | import 'package:alchemist/src/golden_test_runner.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | class _TextCustomPainter extends CustomPainter { 8 | const _TextCustomPainter(); 9 | 10 | @override 11 | void paint(Canvas canvas, Size size) { 12 | final paragraphBuilder = ui.ParagraphBuilder(ui.ParagraphStyle()) 13 | ..pushStyle(ui.TextStyle(color: const Color(0xFF0000FF))) 14 | ..addText('blue text'); 15 | final paragraph = paragraphBuilder.build() 16 | ..layout(ui.ParagraphConstraints(width: size.width)); 17 | canvas.drawParagraph(paragraph, Offset.zero); 18 | } 19 | 20 | @override 21 | bool shouldRepaint(covariant _TextCustomPainter oldDelegate) { 22 | return true; 23 | } 24 | } 25 | 26 | void main() { 27 | group('BlockedTextPaintingContext', () { 28 | const goldenFilePath = 'blocked_text_image_reference.png'; 29 | 30 | Future setUpSurface(WidgetTester tester) async { 31 | final originalSize = tester.view.physicalSize; 32 | const adjustedSize = Size(250, 100); 33 | 34 | tester.view.physicalSize = adjustedSize; 35 | await tester.binding.setSurfaceSize(adjustedSize); 36 | tester.view.devicePixelRatio = 1.0; 37 | 38 | addTearDown(tester.view.resetPhysicalSize); 39 | addTearDown(() => tester.binding.setSurfaceSize(originalSize)); 40 | addTearDown(tester.view.resetDevicePixelRatio); 41 | } 42 | 43 | Widget buildSubject({Key? key}) { 44 | return MaterialApp( 45 | key: key, 46 | debugShowCheckedModeBanner: false, 47 | home: const Scaffold( 48 | backgroundColor: Color(0x0f000000), 49 | body: Padding( 50 | padding: EdgeInsets.all(8), 51 | child: Column( 52 | crossAxisAlignment: CrossAxisAlignment.start, 53 | children: [ 54 | Text('black text', style: TextStyle(color: Color(0xFF000000))), 55 | SizedBox(height: 3), 56 | Text('red text', style: TextStyle(color: Color(0xFFFF0000))), 57 | SizedBox(height: 3), 58 | CustomPaint(painter: _TextCustomPainter(), size: Size(250, 20)), 59 | ], 60 | ), 61 | ), 62 | ), 63 | ); 64 | } 65 | 66 | testWidgets('paints paragraphs in colored blocks', (tester) async { 67 | await setUpSurface(tester); 68 | 69 | const rootKey = Key('root'); 70 | await tester.pumpWidget(buildSubject(key: rootKey)); 71 | 72 | final image = await goldenTestAdapter.getBlockedTextImage( 73 | finder: find.byKey(rootKey), 74 | tester: tester, 75 | ); 76 | 77 | await expectLater(image, matchesGoldenFile(goldenFilePath)); 78 | }); 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /example/example.md: -------------------------------------------------------------------------------- 1 | # Alchemist Example 2 | 3 | ## Recommended Setup Guide 4 | 5 | For a more detailed explanation on how Betterment uses Alchemist, read the included [Recommended Setup Guide][setup-guide]. 6 | 7 | ## Full Example Project 8 | 9 | A full project containing an application containing exemplary widgets and golden tests is included in the [example][example_dir] folder. 10 | 11 | ## Basic usage 12 | 13 | In your project's `test/` directory, add a file for your widget's tests. Then, write and run golden tests by using the `goldenTest` function. 14 | 15 | We recommend putting all golden tests related to the same component into a test `group`. 16 | 17 | Every `goldenTest` commonly contains a group of scenarios related to each other (for example, all scenarios that test the same constructor or widget in a particular context). 18 | 19 | This example shows a basic golden test for `ListTile`s that makes use of some of the more advanced features of the `goldenTest` API to control the output of the test. 20 | 21 | ```dart 22 | import 'package:alchemist/alchemist.dart'; 23 | import 'package:flutter/material.dart'; 24 | import 'package:flutter_test/flutter_test.dart'; 25 | 26 | void main() { 27 | group('ListTile Golden Tests', () { 28 | goldenTest( 29 | 'renders correctly', 30 | fileName: 'list_tile', 31 | builder: () => GoldenTestGroup( 32 | scenarioConstraints: const BoxConstraints(maxWidth: 600), 33 | children: [ 34 | GoldenTestScenario( 35 | name: 'with title', 36 | child: const ListTile( 37 | title: Text('ListTile.title'), 38 | ), 39 | ), 40 | GoldenTestScenario( 41 | name: 'with title and subtitle', 42 | child: const ListTile( 43 | title: Text('ListTile.title'), 44 | subtitle: Text('ListTile.subtitle'), 45 | ), 46 | ), 47 | GoldenTestScenario( 48 | name: 'with trailing icon', 49 | child: const ListTile( 50 | title: Text('ListTile.title'), 51 | trailing: Icon(Icons.chevron_right_rounded), 52 | ), 53 | ), 54 | ], 55 | ), 56 | ); 57 | }); 58 | } 59 | ``` 60 | 61 | Then, simply run Flutter test and pass the `--update-goldens` flag to generate the golden files. 62 | 63 | ```shell 64 | flutter test --update-goldens 65 | ``` 66 | 67 | ## gitignore 68 | 69 | We recommend adding the following lines to your project's `.gitignore` file to prevent platform-specific artifacts from being included in your git repository. 70 | 71 | ```gitignore 72 | # Ignore platform-specific goldens 73 | **/goldens/macos 74 | **/goldens/linux 75 | **/goldens/windows 76 | ``` 77 | 78 | [setup-guide]: https://github.com/Betterment/alchemist/blob/main/RECOMMENDED_SETUP_GUIDE.md 79 | [example_dir]: https://github.com/Betterment/alchemist/tree/main/example 80 | -------------------------------------------------------------------------------- /test/src/alchemist_test_variant_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:alchemist/src/alchemist_config.dart'; 2 | import 'package:alchemist/src/alchemist_test_variant.dart'; 3 | import 'package:alchemist/src/host_platform.dart'; 4 | import 'package:flutter/widgets.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | import 'package:mocktail/mocktail.dart'; 7 | 8 | class MockAlchemistConfig extends Mock implements AlchemistConfig {} 9 | 10 | class MockPlatformGoldensConfig extends Mock implements PlatformGoldensConfig {} 11 | 12 | class MockCiGoldensConfig extends Mock implements CiGoldensConfig {} 13 | 14 | class FakeImageStreamCompleter extends ImageStreamCompleter {} 15 | 16 | void main() { 17 | group('AlchemistTestVariant', () { 18 | test('returns values', () { 19 | const platform = HostPlatform.linux; 20 | final ciConfig = MockCiGoldensConfig(); 21 | when(() => ciConfig.enabled).thenReturn(true); 22 | final platformConfig = MockPlatformGoldensConfig(); 23 | when(() => platformConfig.enabled).thenReturn(true); 24 | when(() => platformConfig.platforms).thenReturn({platform}); 25 | final config = MockAlchemistConfig(); 26 | when(() => config.platformGoldensConfig).thenReturn(platformConfig); 27 | when(() => config.ciGoldensConfig).thenReturn(ciConfig); 28 | final variant = AlchemistTestVariant( 29 | config: config, 30 | currentPlatform: platform, 31 | ); 32 | expect(variant.values, {platformConfig, ciConfig}); 33 | }); 34 | 35 | group('Lifecycle', () { 36 | late AlchemistTestVariant variant; 37 | late MockAlchemistConfig config; 38 | 39 | setUp(() { 40 | config = MockAlchemistConfig(); 41 | variant = AlchemistTestVariant( 42 | config: config, 43 | currentPlatform: HostPlatform.linux, 44 | ); 45 | }); 46 | 47 | test('instantiates', () { 48 | expect(variant, isA()); 49 | }); 50 | 51 | test('tearDown clears the image cache', () async { 52 | TestWidgetsFlutterBinding.ensureInitialized(); 53 | imageCache.putIfAbsent('key', FakeImageStreamCompleter.new); 54 | expect(imageCache.containsKey('key'), isTrue); 55 | await variant.tearDown(MockCiGoldensConfig(), null); 56 | expect(imageCache.containsKey('key'), isFalse); 57 | }); 58 | 59 | test('setUp sets current value', () async { 60 | final value = MockCiGoldensConfig(); 61 | await expectLater(variant.setUp(value), completes); 62 | expect(variant.currentConfig, value); 63 | }); 64 | 65 | test('describeValue returns environment name', () async { 66 | final value = MockCiGoldensConfig(); 67 | const environmentName = 'TEST'; 68 | when(() => value.environmentName).thenReturn(environmentName); 69 | expect(variant.describeValue(value), environmentName); 70 | }); 71 | }); 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /lib/src/pumps.dart: -------------------------------------------------------------------------------- 1 | import 'package:alchemist/alchemist.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | /// A function that may perform pumping actions to prime a golden test. 6 | /// 7 | /// Used in the [goldenTest] function to perform any actions necessary to prime 8 | /// the widget tree before the golden test is compared or generated. 9 | typedef PumpAction = Future Function(WidgetTester tester); 10 | 11 | /// A function used to render a given [Widget]. 12 | typedef PumpWidget = Future Function(WidgetTester tester, Widget widget); 13 | 14 | /// Returns a custom [PumpAction] that pumps the widget tree [n] times before 15 | /// golden evaluation. 16 | /// 17 | /// See [PumpAction] for more details. 18 | PumpAction pumpNTimes(int n, [Duration? duration]) { 19 | return (tester) async { 20 | for (var i = 0; i < n; i++) { 21 | await tester.pump(duration); 22 | } 23 | }; 24 | } 25 | 26 | /// A custom [PumpAction] that pumps the widget tree once before golden 27 | /// evaluation. 28 | /// 29 | /// See [PumpAction] for more details. 30 | final pumpOnce = pumpNTimes(1); 31 | 32 | /// A custom [PumpAction] that pumps and settles the widget tree before golden 33 | /// evaluation. 34 | /// 35 | /// See [PumpAction] for more details. 36 | Future onlyPumpAndSettle(WidgetTester tester) => tester.pumpAndSettle(); 37 | 38 | /// A custom [PumpAction] to ensure that the images for all [Image], 39 | /// [FadeInImage], and [DecoratedBox] widgets are loaded before the golden file 40 | /// is generated. 41 | /// 42 | /// See [PumpAction] for more details. 43 | Future precacheImages(WidgetTester tester) async { 44 | await tester.runAsync(() async { 45 | final images = >[]; 46 | for (final element in find.byType(Image).evaluate()) { 47 | final widget = element.widget as Image; 48 | final image = widget.image; 49 | images.add(precacheImage(image, element)); 50 | } 51 | for (final element in find.byType(FadeInImage).evaluate()) { 52 | final widget = element.widget as FadeInImage; 53 | final image = widget.image; 54 | images.add(precacheImage(image, element)); 55 | } 56 | for (final element in find.byType(DecoratedBox).evaluate()) { 57 | final widget = element.widget as DecoratedBox; 58 | final decoration = widget.decoration; 59 | if (decoration is BoxDecoration && decoration.image != null) { 60 | final image = decoration.image!.image; 61 | images.add(precacheImage(image, element)); 62 | } 63 | } 64 | await Future.wait(images); 65 | }); 66 | await tester.pumpAndSettle(); 67 | } 68 | 69 | /// A custom [PumpWidget] that pumps the widget tree before golden 70 | /// evaluation, analogous to [WidgetTester.pumpWidget]. 71 | /// 72 | /// See [PumpWidget] for more details. 73 | Future onlyPumpWidget(WidgetTester tester, Widget widget) { 74 | return tester.pumpWidget(widget); 75 | } 76 | -------------------------------------------------------------------------------- /lib/src/golden_test_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:alchemist/src/golden_test_group.dart'; 2 | import 'package:alchemist/src/golden_test_scenario.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | /// {@template golden_test_theme} 6 | /// A theme that dictates the behavior and appearance of elements created 7 | /// by Alchemist during golden testing. This theme is used to ensure that 8 | /// parts of golden tests controlled by Alchemist are consistent across 9 | /// Flutter SDK versions. 10 | /// {@endtemplate} 11 | class GoldenTestTheme extends ThemeExtension { 12 | /// {@macro golden_test_theme} 13 | GoldenTestTheme({ 14 | required this.backgroundColor, 15 | required this.borderColor, 16 | required this.nameTextStyle, 17 | this.padding = EdgeInsets.zero, 18 | }); 19 | 20 | /// The standard theme for golden tests, used when no other theme is provided. 21 | factory GoldenTestTheme.standard() { 22 | return GoldenTestTheme( 23 | // These colors are not tied to any implementation so they won't 24 | // change out from under us, which would cause golden tests to fail. 25 | backgroundColor: const Color(0xFF2b54a1), 26 | borderColor: const Color(0xFF3d394a), 27 | nameTextStyle: const TextStyle(fontSize: 18), 28 | ); 29 | } 30 | 31 | /// The background color of the golden test. 32 | final Color backgroundColor; 33 | 34 | /// The border color used to separate scenarios in a [GoldenTestGroup]. 35 | final Color borderColor; 36 | 37 | /// The padding that is used to wrap around: 38 | /// - the whole image 39 | /// - each individual [GoldenTestScenario] 40 | final EdgeInsetsGeometry padding; 41 | 42 | /// The text style that is used to show the name in a [GoldenTestScenario] 43 | final TextStyle nameTextStyle; 44 | 45 | @override 46 | ThemeExtension copyWith({ 47 | Color? backgroundColor, 48 | Color? borderColor, 49 | EdgeInsetsGeometry? padding, 50 | TextStyle? nameTextStyle, 51 | }) { 52 | return GoldenTestTheme( 53 | backgroundColor: backgroundColor ?? this.backgroundColor, 54 | borderColor: borderColor ?? this.borderColor, 55 | padding: padding ?? this.padding, 56 | nameTextStyle: nameTextStyle ?? this.nameTextStyle, 57 | ); 58 | } 59 | 60 | @override 61 | ThemeExtension lerp( 62 | covariant ThemeExtension? other, 63 | double t, 64 | ) { 65 | if (other is! GoldenTestTheme) { 66 | return this; 67 | } 68 | return GoldenTestTheme( 69 | backgroundColor: 70 | Color.lerp(backgroundColor, other.backgroundColor, t) ?? 71 | backgroundColor, 72 | borderColor: Color.lerp(borderColor, other.borderColor, t) ?? borderColor, 73 | padding: EdgeInsetsGeometry.lerp(padding, other.padding, t) ?? padding, 74 | nameTextStyle: nameTextStyle.copyWith( 75 | color: 76 | Color.lerp(nameTextStyle.color, other.nameTextStyle.color, t) ?? 77 | nameTextStyle.color, 78 | ), 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /.github/workflows/update_goldens.yaml: -------------------------------------------------------------------------------- 1 | name: Update Goldens 2 | # ignore: RiskyTriggers 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | branch: 7 | description: "Branch to generate goldens on" 8 | required: true 9 | flutter_version: 10 | description: "Flutter version to use" 11 | required: true 12 | 13 | permissions: 14 | contents: write 15 | 16 | jobs: 17 | update_goldens: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout repo 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | 23 | - name: Validate branch input 24 | run: | 25 | BRANCH_PATTERN="^[a-zA-Z0-9/_.-]+$" 26 | 27 | echo "Checking branch name: $BRANCH_NAME" 28 | 29 | # Validate branch name against the regex pattern 30 | if [[ $BRANCH_NAME =~ $BRANCH_PATTERN ]]; then 31 | echo "Branch name is valid." 32 | exit 0 33 | else 34 | echo "Branch name is invalid." 35 | exit 1 36 | fi 37 | env: 38 | BRANCH_NAME: ${{ inputs.branch }} 39 | 40 | - name: Validate Flutter version input 41 | run: | 42 | VERSION_PATTERN="^[0-9]+\.[0-9]+\.[0-9]+$" 43 | 44 | echo "Checking version input: $VERSION" 45 | 46 | # Validate branch name against the regex pattern 47 | if [[ $VERSION =~ $VERSION_PATTERN ]]; then 48 | echo "Version input is valid." 49 | exit 0 50 | else 51 | echo "Version input is invalid." 52 | exit 1 53 | fi 54 | env: 55 | VERSION: ${{ inputs.flutter_version }} 56 | 57 | - name: Ensure branch is not main 58 | if: ${{ github.event.inputs.branch == 'main' || github.event.inputs.branch == 'origin/main'}} 59 | run: exit 1 60 | 61 | - name: Checkout branch 62 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 63 | with: 64 | # Branch validated above 65 | # ignore: UnsafeCheckout 66 | ref: ${{ github.event.inputs.branch }} 67 | 68 | - uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0 69 | with: 70 | flutter-version: ${{ github.event.inputs.flutter_version }} 71 | channel: any 72 | cache: true 73 | 74 | - name: Disable animations 75 | run: flutter config --no-cli-animations 76 | 77 | - name: Update Goldens 78 | run: | 79 | flutter test --update-goldens 80 | continue-on-error: true 81 | 82 | - name: Commit Changes 83 | id: commit_changes 84 | env: 85 | BRANCH_NAME: ${{ github.event.inputs.branch }} 86 | run: | 87 | git config --local user.name "github-actions[bot]" 88 | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" 89 | git add . 90 | git diff-index --quiet HEAD || git commit -m "chore: Updating Goldens" 91 | git push origin HEAD:"$BRANCH_NAME" 92 | -------------------------------------------------------------------------------- /example/linux/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.10) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | 12 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...), 13 | # which isn't available in 3.10. 14 | function(list_prepend LIST_NAME PREFIX) 15 | set(NEW_LIST "") 16 | foreach(element ${${LIST_NAME}}) 17 | list(APPEND NEW_LIST "${PREFIX}${element}") 18 | endforeach(element) 19 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) 20 | endfunction() 21 | 22 | # === Flutter Library === 23 | # System-level dependencies. 24 | find_package(PkgConfig REQUIRED) 25 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 26 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) 27 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) 28 | 29 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") 30 | 31 | # Published to parent scope for install step. 32 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 33 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 34 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 35 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) 36 | 37 | list(APPEND FLUTTER_LIBRARY_HEADERS 38 | "fl_basic_message_channel.h" 39 | "fl_binary_codec.h" 40 | "fl_binary_messenger.h" 41 | "fl_dart_project.h" 42 | "fl_engine.h" 43 | "fl_json_message_codec.h" 44 | "fl_json_method_codec.h" 45 | "fl_message_codec.h" 46 | "fl_method_call.h" 47 | "fl_method_channel.h" 48 | "fl_method_codec.h" 49 | "fl_method_response.h" 50 | "fl_plugin_registrar.h" 51 | "fl_plugin_registry.h" 52 | "fl_standard_message_codec.h" 53 | "fl_standard_method_codec.h" 54 | "fl_string_codec.h" 55 | "fl_value.h" 56 | "fl_view.h" 57 | "flutter_linux.h" 58 | ) 59 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") 60 | add_library(flutter INTERFACE) 61 | target_include_directories(flutter INTERFACE 62 | "${EPHEMERAL_DIR}" 63 | ) 64 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") 65 | target_link_libraries(flutter INTERFACE 66 | PkgConfig::GTK 67 | PkgConfig::GLIB 68 | PkgConfig::GIO 69 | ) 70 | add_dependencies(flutter flutter_assemble) 71 | 72 | # === Flutter tool backend === 73 | # _phony_ is a non-existent file to force this command to run every time, 74 | # since currently there's no way to get a full input/output list from the 75 | # flutter tool. 76 | add_custom_command( 77 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 78 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_ 79 | COMMAND ${CMAKE_COMMAND} -E env 80 | ${FLUTTER_TOOL_ENVIRONMENT} 81 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" 82 | ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} 83 | VERBATIM 84 | ) 85 | add_custom_target(flutter_assemble DEPENDS 86 | "${FLUTTER_LIBRARY}" 87 | ${FLUTTER_LIBRARY_HEADERS} 88 | ) 89 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/src/host_platform.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:equatable/equatable.dart'; 4 | 5 | /// Default host platform (the current host machine platform). 6 | final defaultHostPlatform = HostPlatform._realPlatform(); 7 | HostPlatform _hostPlatform = defaultHostPlatform; 8 | 9 | /// Indicates the current host platform used by Alchemist. 10 | /// Can be overridden for testing. This value is utilized by 11 | /// [HostPlatform.current]. 12 | HostPlatform get hostPlatform => _hostPlatform; 13 | set hostPlatform(HostPlatform value) => _hostPlatform = value; 14 | 15 | /// A class that represents a host platform that can run golden tests. 16 | /// 17 | /// The current platform can be retrieved using [HostPlatform.current], and 18 | /// checks against this value are available using [isMacOS], [isLinux] and so 19 | /// on. 20 | class HostPlatform extends Equatable implements Comparable { 21 | const HostPlatform._(this._value); 22 | 23 | /// An internal factory used to retrieve a [HostPlatform] based on the current 24 | /// real platform this program is running on. 25 | factory HostPlatform._realPlatform() { 26 | final hostByPlatform = { 27 | Platform.isMacOS: HostPlatform.macOS, 28 | Platform.isLinux: HostPlatform.linux, 29 | Platform.isWindows: HostPlatform.windows, 30 | }; 31 | 32 | return hostByPlatform[true]!; 33 | } 34 | 35 | /// The current host platform. 36 | factory HostPlatform.current() { 37 | return hostPlatform; 38 | } 39 | 40 | /// The internal value used to represent the current platform. 41 | /// 42 | /// This value is also returned by [operatingSystem]. 43 | final String _value; 44 | 45 | /// Returns all values [HostPlatform] can represent. 46 | static final values = {macOS, linux, windows}; 47 | 48 | /// The Apple macOS platform (`"macOS"`). 49 | /// 50 | /// See [HostPlatform] for more information. 51 | static const macOS = HostPlatform._('macOS'); 52 | 53 | /// The Linux platform (`"Linux"`). 54 | /// 55 | /// See [HostPlatform] for more information. 56 | static const linux = HostPlatform._('Linux'); 57 | 58 | /// The Microsoft Windows platform (`"Windows"`). 59 | /// 60 | /// See [HostPlatform] for more information. 61 | static const windows = HostPlatform._('Windows'); 62 | 63 | /// The name of the current platform. 64 | /// 65 | /// This will return the name of this [HostPlatform]. 66 | /// 67 | /// ```dart 68 | /// HostPlatform.macOS.operatingSystem; // "macOS" 69 | /// HostPlatform.linux.operatingSystem; // "Linux" 70 | /// HostPlatform.windows.operatingSystem; // "Windows" 71 | /// ``` 72 | String get operatingSystem => _value; 73 | 74 | /// Indicates whether this platform is Apple's macOS. 75 | bool get isMacOS => this == HostPlatform.macOS; 76 | 77 | /// Indicates whether this platform is Linux. 78 | bool get isLinux => this == HostPlatform.linux; 79 | 80 | /// Indicates whether this platform is Microsoft's Windows. 81 | bool get isWindows => this == HostPlatform.windows; 82 | 83 | @override 84 | String toString() { 85 | return 'HostPlatform($_value)'; 86 | } 87 | 88 | @override 89 | List get props => [_value]; 90 | 91 | @override 92 | int compareTo(covariant HostPlatform other) => _value.compareTo(other._value); 93 | } 94 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: alchemist 2 | 3 | on: [pull_request] 4 | 5 | env: 6 | FLUTTER_VERSION: 3.16.0 7 | 8 | jobs: 9 | analyze: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 14 | 15 | - name: Determine Flutter version 16 | id: flutter_version 17 | run: | 18 | # Grab the Flutter version from the .fvmrc file 19 | FLUTTER_VERSION=$(grep '"flutter"' .fvmrc | grep -o '[0-9][0-9.]*') 20 | echo "flutter_version=$FLUTTER_VERSION" >> "$GITHUB_OUTPUT" 21 | 22 | - uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0 23 | with: 24 | flutter-version: ${{ steps.flutter_version.outputs.flutter_version }} 25 | cache: true 26 | 27 | - name: Install Dependencies 28 | run: flutter packages get 29 | 30 | - name: Format 31 | run: dart format --set-exit-if-changed . 32 | 33 | - name: Analyze project source 34 | uses: invertase/github-action-dart-analyzer@e981b01a458d0bab71ee5da182e5b26687b7101b # v3.0.0 35 | 36 | - uses: codecov/codecov-action@c2fcb216de2b0348de0100baa3ea2cad9f100a01 # v5.1.0 37 | with: 38 | files: coverage/lcov.info 39 | 40 | test: 41 | runs-on: ubuntu-latest 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | flutter-version: ["3.32.8", "3.35.6"] 46 | steps: 47 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 48 | 49 | - uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0 50 | with: 51 | flutter-version: ${{ matrix.flutter-version }} 52 | channel: "stable" 53 | cache: true 54 | 55 | - name: Install Dependencies 56 | run: flutter packages get 57 | 58 | - name: Disable animations 59 | run: flutter config --no-cli-animations 60 | 61 | - name: Run tests 62 | run: | 63 | flutter test --no-pub --coverage --test-randomize-ordering-seed=random 64 | 65 | - name: Upload failures 66 | if: failure() 67 | uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 68 | with: 69 | name: "golden_failures_${{ matrix.flutter-version }}" 70 | path: | 71 | **/failures/**/*.png 72 | 73 | pana: 74 | runs-on: ubuntu-latest 75 | 76 | steps: 77 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 78 | 79 | - name: Determine Flutter version 80 | id: flutter_version 81 | run: | 82 | # Grab the Flutter version from the .fvmrc file 83 | FLUTTER_VERSION=$(grep '"flutter"' .fvmrc | grep -o '[0-9][0-9.]*') 84 | echo "flutter_version=$FLUTTER_VERSION" >> "$GITHUB_OUTPUT" 85 | 86 | - uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0 87 | with: 88 | flutter-version: ${{ steps.flutter_version.outputs.flutter_version }} 89 | cache: true 90 | 91 | - name: Install Dependencies 92 | run: | 93 | flutter packages get 94 | flutter pub global activate pana 95 | 96 | - name: Verify Pub Score 97 | run: ./tool/verify_pub_score.sh 130 98 | -------------------------------------------------------------------------------- /test/src/interactions_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:alchemist/src/interactions.dart'; 2 | import 'package:flutter/gestures.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/rendering.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | void main() { 8 | Widget buildWrapper(Widget child) { 9 | return MaterialApp(home: Scaffold(body: child)); 10 | } 11 | 12 | group('press', () { 13 | testWidgets('press presses correctly', (tester) async { 14 | var onPressedCalled = false; 15 | await tester.pumpWidget( 16 | buildWrapper( 17 | ElevatedButton( 18 | onPressed: () { 19 | onPressedCalled = true; 20 | }, 21 | child: const Text('button'), 22 | ), 23 | ), 24 | ); 25 | final cleanup = await press(find.byType(ElevatedButton))(tester); 26 | await cleanup?.call(); 27 | expect(onPressedCalled, isTrue); 28 | }); 29 | 30 | testWidgets('press for custom time pumps correctly', (tester) async { 31 | await tester.pumpWidget( 32 | buildWrapper( 33 | ElevatedButton(onPressed: () {}, child: const Text('button')), 34 | ), 35 | ); 36 | const holdForDuration = Duration(seconds: 3); 37 | // Since `testWidgets` runs inside a `fakeAsync` context, we can grab the 38 | // fake starting time. Pumping for a certain [Duration] advances the 39 | // fake test clock, allowing us to verify we are pumping correctly 40 | // in the interactions code. 41 | final startTime = tester.binding.clock.now(); 42 | final cleanup = await press( 43 | find.byType(ElevatedButton), 44 | holdFor: holdForDuration, 45 | )(tester); 46 | await cleanup?.call(); 47 | final elapsed = tester.binding.clock.now().difference(startTime); 48 | expect(elapsed, greaterThanOrEqualTo(holdForDuration + kPressTimeout)); 49 | }); 50 | }); 51 | 52 | testWidgets('longPress', (tester) async { 53 | var onLongPressedCalled = false; 54 | await tester.pumpWidget( 55 | buildWrapper( 56 | ElevatedButton( 57 | onPressed: null, 58 | onLongPress: () { 59 | onLongPressedCalled = true; 60 | }, 61 | child: const Text('button'), 62 | ), 63 | ), 64 | ); 65 | final cleanup = await longPress(find.byType(ElevatedButton))(tester); 66 | await cleanup?.call(); 67 | expect(onLongPressedCalled, isTrue); 68 | }); 69 | 70 | testWidgets('scroll', (tester) async { 71 | await tester.pumpWidget( 72 | buildWrapper( 73 | ListView.builder( 74 | // this `itemCount` is long enough to reach the `dragOffset` 75 | // we're using on our scroll interaction. 76 | itemCount: 20, 77 | itemBuilder: (context, index) { 78 | return ListTile(title: Text('item $index')); 79 | }, 80 | ), 81 | ), 82 | ); 83 | 84 | const dragOffset = 100.0; 85 | 86 | final cleanup = await scroll( 87 | find.byType(Scrollable), 88 | offset: const Offset(0, -dragOffset), 89 | )(tester); 90 | 91 | await cleanup?.call(); 92 | 93 | final viewportFinder = find.byType(Viewport); 94 | final viewport = tester.renderObject(viewportFinder) as RenderViewport; 95 | 96 | expect(viewport.offset.pixels, dragOffset); 97 | }); 98 | } 99 | -------------------------------------------------------------------------------- /example/windows/runner/Runner.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #pragma code_page(65001) 4 | #include "resource.h" 5 | 6 | #define APSTUDIO_READONLY_SYMBOLS 7 | ///////////////////////////////////////////////////////////////////////////// 8 | // 9 | // Generated from the TEXTINCLUDE 2 resource. 10 | // 11 | #include "winres.h" 12 | 13 | ///////////////////////////////////////////////////////////////////////////// 14 | #undef APSTUDIO_READONLY_SYMBOLS 15 | 16 | ///////////////////////////////////////////////////////////////////////////// 17 | // English (United States) resources 18 | 19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | IDI_APP_ICON ICON "resources\\app_icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | #ifdef FLUTTER_BUILD_NUMBER 64 | #define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,0 67 | #endif 68 | 69 | #ifdef FLUTTER_BUILD_NAME 70 | #define VERSION_AS_STRING #FLUTTER_BUILD_NAME 71 | #else 72 | #define VERSION_AS_STRING "1.0.0" 73 | #endif 74 | 75 | VS_VERSION_INFO VERSIONINFO 76 | FILEVERSION VERSION_AS_NUMBER 77 | PRODUCTVERSION VERSION_AS_NUMBER 78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 79 | #ifdef _DEBUG 80 | FILEFLAGS VS_FF_DEBUG 81 | #else 82 | FILEFLAGS 0x0L 83 | #endif 84 | FILEOS VOS__WINDOWS32 85 | FILETYPE VFT_APP 86 | FILESUBTYPE 0x0L 87 | BEGIN 88 | BLOCK "StringFileInfo" 89 | BEGIN 90 | BLOCK "040904e4" 91 | BEGIN 92 | VALUE "CompanyName", "com.betterment" "\0" 93 | VALUE "FileDescription", "example" "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "example" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2022 com.betterment. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "example.exe" "\0" 98 | VALUE "ProductName", "example" "\0" 99 | VALUE "ProductVersion", VERSION_AS_STRING "\0" 100 | END 101 | END 102 | BLOCK "VarFileInfo" 103 | BEGIN 104 | VALUE "Translation", 0x409, 1252 105 | END 106 | END 107 | 108 | #endif // English (United States) resources 109 | ///////////////////////////////////////////////////////////////////////////// 110 | 111 | 112 | 113 | #ifndef APSTUDIO_INVOKED 114 | ///////////////////////////////////////////////////////////////////////////// 115 | // 116 | // Generated from the TEXTINCLUDE 3 resource. 117 | // 118 | 119 | 120 | ///////////////////////////////////////////////////////////////////////////// 121 | #endif // not APSTUDIO_INVOKED 122 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /example/windows/runner/win32_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_WIN32_WINDOW_H_ 2 | #define RUNNER_WIN32_WINDOW_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be 11 | // inherited from by classes that wish to specialize with custom 12 | // rendering and input handling 13 | class Win32Window { 14 | public: 15 | struct Point { 16 | unsigned int x; 17 | unsigned int y; 18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {} 19 | }; 20 | 21 | struct Size { 22 | unsigned int width; 23 | unsigned int height; 24 | Size(unsigned int width, unsigned int height) 25 | : width(width), height(height) {} 26 | }; 27 | 28 | Win32Window(); 29 | virtual ~Win32Window(); 30 | 31 | // Creates and shows a win32 window with |title| and position and size using 32 | // |origin| and |size|. New windows are created on the default monitor. Window 33 | // sizes are specified to the OS in physical pixels, hence to ensure a 34 | // consistent size to will treat the width height passed in to this function 35 | // as logical pixels and scale to appropriate for the default monitor. Returns 36 | // true if the window was created successfully. 37 | bool CreateAndShow(const std::wstring& title, 38 | const Point& origin, 39 | const Size& size); 40 | 41 | // Release OS resources associated with window. 42 | void Destroy(); 43 | 44 | // Inserts |content| into the window tree. 45 | void SetChildContent(HWND content); 46 | 47 | // Returns the backing Window handle to enable clients to set icon and other 48 | // window properties. Returns nullptr if the window has been destroyed. 49 | HWND GetHandle(); 50 | 51 | // If true, closing this window will quit the application. 52 | void SetQuitOnClose(bool quit_on_close); 53 | 54 | // Return a RECT representing the bounds of the current client area. 55 | RECT GetClientArea(); 56 | 57 | protected: 58 | // Processes and route salient window messages for mouse handling, 59 | // size change and DPI. Delegates handling of these to member overloads that 60 | // inheriting classes can handle. 61 | virtual LRESULT MessageHandler(HWND window, 62 | UINT const message, 63 | WPARAM const wparam, 64 | LPARAM const lparam) noexcept; 65 | 66 | // Called when CreateAndShow is called, allowing subclass window-related 67 | // setup. Subclasses should return false if setup fails. 68 | virtual bool OnCreate(); 69 | 70 | // Called when Destroy is called. 71 | virtual void OnDestroy(); 72 | 73 | private: 74 | friend class WindowClassRegistrar; 75 | 76 | // OS callback called by message pump. Handles the WM_NCCREATE message which 77 | // is passed when the non-client area is being created and enables automatic 78 | // non-client DPI scaling so that the non-client area automatically 79 | // responsponds to changes in DPI. All other messages are handled by 80 | // MessageHandler. 81 | static LRESULT CALLBACK WndProc(HWND const window, 82 | UINT const message, 83 | WPARAM const wparam, 84 | LPARAM const lparam) noexcept; 85 | 86 | // Retrieves a class instance pointer for |window| 87 | static Win32Window* GetThisFromHandle(HWND const window) noexcept; 88 | 89 | bool quit_on_close_ = false; 90 | 91 | // window handle for top level window. 92 | HWND window_handle_ = nullptr; 93 | 94 | // window handle for hosted content. 95 | HWND child_content_ = nullptr; 96 | }; 97 | 98 | #endif // RUNNER_WIN32_WINDOW_H_ 99 | -------------------------------------------------------------------------------- /test/src/host_platform_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:alchemist/alchemist.dart'; 4 | import 'package:mocktail/mocktail.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | class MockHostPlatform extends Mock implements HostPlatform {} 8 | 9 | void main() { 10 | group('hostPlatform override', () { 11 | test('overrides and returns the given value', () { 12 | final nextHostPlatform = HostPlatform.values.firstWhere( 13 | (platform) => platform != hostPlatform, 14 | ); 15 | hostPlatform = nextHostPlatform; 16 | expect(hostPlatform, nextHostPlatform); 17 | hostPlatform = defaultHostPlatform; 18 | }); 19 | }); 20 | 21 | group('HostPlatform', () { 22 | group('.current', () { 23 | test( 24 | 'returns current host platform', 25 | () { 26 | if (Platform.isMacOS) { 27 | expect(HostPlatform.current(), HostPlatform.macOS); 28 | } else if (Platform.isLinux) { 29 | expect(HostPlatform.current(), HostPlatform.linux); 30 | } else if (Platform.isWindows) { 31 | expect(HostPlatform.current(), HostPlatform.windows); 32 | } 33 | }, 34 | skip: !Platform.isMacOS && !Platform.isLinux && !Platform.isWindows, 35 | ); 36 | }); 37 | 38 | test('has correct string representation', () { 39 | expect(HostPlatform.macOS.toString(), 'HostPlatform(macOS)'); 40 | }); 41 | 42 | group('compareTo', () { 43 | test('is correct when compared to self', () { 44 | expect(HostPlatform.macOS.compareTo(HostPlatform.macOS), 0); 45 | expect(HostPlatform.linux.compareTo(HostPlatform.linux), 0); 46 | expect(HostPlatform.windows.compareTo(HostPlatform.windows), 0); 47 | }); 48 | 49 | test('is consistent', () { 50 | expect( 51 | HostPlatform.macOS.compareTo(HostPlatform.linux), 52 | -1 * HostPlatform.linux.compareTo(HostPlatform.macOS), 53 | ); 54 | expect( 55 | HostPlatform.macOS.compareTo(HostPlatform.windows), 56 | -1 * HostPlatform.windows.compareTo(HostPlatform.macOS), 57 | ); 58 | expect( 59 | HostPlatform.windows.compareTo(HostPlatform.linux), 60 | -1 * HostPlatform.linux.compareTo(HostPlatform.windows), 61 | ); 62 | }); 63 | }); 64 | 65 | group('macOS', () { 66 | test('returns true for platform check', () { 67 | expect(HostPlatform.macOS.isMacOS, isTrue); 68 | 69 | expect(HostPlatform.macOS.isLinux, isFalse); 70 | expect(HostPlatform.macOS.isWindows, isFalse); 71 | }); 72 | 73 | test('has correct operating system name', () { 74 | expect(HostPlatform.macOS.operatingSystem, 'macOS'); 75 | }); 76 | }); 77 | 78 | group('linux', () { 79 | test('returns true for platform check', () { 80 | expect(HostPlatform.linux.isLinux, isTrue); 81 | 82 | expect(HostPlatform.linux.isMacOS, isFalse); 83 | expect(HostPlatform.linux.isWindows, isFalse); 84 | }); 85 | 86 | test('has correct operating system name', () { 87 | expect(HostPlatform.linux.operatingSystem, 'Linux'); 88 | }); 89 | }); 90 | 91 | group('windows', () { 92 | test('returns true for platform check', () { 93 | expect(HostPlatform.windows.isWindows, isTrue); 94 | 95 | expect(HostPlatform.windows.isLinux, isFalse); 96 | expect(HostPlatform.windows.isMacOS, isFalse); 97 | }); 98 | 99 | test('has correct operating system name', () { 100 | expect(HostPlatform.windows.operatingSystem, 'Windows'); 101 | }); 102 | }); 103 | }); 104 | } 105 | -------------------------------------------------------------------------------- /RECOMMENDED_SETUP_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Alchemist -- Recommended Setup Guide 2 | 3 | This document outlines the recommended usage for [Alchemist](./README.md), based on how Betterment uses the tool in their internal projects. 4 | 5 | ## Platform tests vs CI tests 6 | 7 | As explained in [the README](./README.md#about-platform-tests-vs-ci-tests), Alchemist knows two kinds of golden tests; platform tests and CI tests. Like many others in the community, we had discovered the unpleasant surprise of how individual platforms render text differently, making golden tests hard to get working consistently between environments. This was the primary reason we built Alchemist. 8 | 9 | By having a separation between tests that are readable and tests and that allow us to make valuable assertions about layout, color and structure, we're able to reap the rewards of having both a human and a computer make sure our components stay consistent over time. 10 | 11 | ## Desired workflow 12 | 13 | We use Alchemist to generate golden files for many of our internal projects. The workflow we use in golden testing is as follows: 14 | 15 | - Any component that we want to test is placed in a file with the format `test/_golden_test.dart`. 16 | - CI goldens are generated in `test/goldens/ci/.png`. These files are used in CI builds to compare the output of these tests to their golden reference files. 17 | - Platform goldens are also generated, and are placed in `test/goldens//.png`. These only function as a reference for developers to see how their platform renders the component, and is useful for debugging visual issues when CI golden tests fail. 18 | 19 | Generally, CI goldens are tracked in source control to make sure CI processes have access to these files and can run the necessary tests. Platform goldens, however, are not tracked in source control, since their output is inherently unstable and partially dependent on the platform they're run on. 20 | 21 | ## Configuration 22 | 23 | To achieve the desired workflow, we configure our tests using a `flutter_test_config.dart` file. If provided, this file will be executed prior to every test in a Dart or Flutter package. You can learn more about this file [here](https://api.flutter.dev/flutter/flutter_test/flutter_test-library.html#per-directory-hierarchy). 24 | 25 | An average test config file for Betterment might look like this: 26 | 27 | ```dart 28 | // flutter_test_config.dart 29 | 30 | import 'dart:async'; 31 | 32 | import 'package:alchemist/alchemist.dart'; 33 | import 'package:custom_styling_package/custom_styling_package.dart'; 34 | 35 | Future testExecutable(FutureOr Function() testMain) async { 36 | // ignore: do_not_use_environment 37 | const isRunningInCi = bool.fromEnvironment('CI', defaultValue: false); 38 | 39 | return AlchemistConfig.runWithConfig( 40 | config: AlchemistConfig( 41 | theme: CustomTheme.light(), 42 | platformGoldensConfig: const PlatformGoldensConfig( 43 | enabled: !isRunningInCi, 44 | ), 45 | ), 46 | run: testMain, 47 | ); 48 | } 49 | ``` 50 | 51 | ## Ignored files 52 | 53 | To ignore any non-CI specific test files, including test failures, a `.gitignore` file in the root of our project is made with the following lines: 54 | 55 | ``` 56 | # Ignore non-CI golden files and failures 57 | test/**/goldens/**/*.* 58 | test/**/failures/**/*.* 59 | !test/**/goldens/ci/*.* 60 | ``` 61 | 62 | ## CI environment 63 | 64 | Since all the necessary setup has been completed in the steps above, no changes in our CI environment are required. On every PR or push to the `main` branch, our CI process runs all tests. 65 | 66 | ```shell 67 | flutter test # Other arguments... 68 | ``` 69 | 70 | Note that it's important **not** to pass the `--update-goldens` flag to the `test` command. This will cause all golden files to be regenerated, which means all tests will by definition always pass. 71 | -------------------------------------------------------------------------------- /example/test/widgets/multiple_icons_golden_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:alchemist/alchemist.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | void main() { 6 | group('Multiple icons test >', () { 7 | final iconsMap = { 8 | 'add': Icons.add, 9 | 'alarm': Icons.alarm, 10 | 'android': Icons.android, 11 | 'arrow_back': Icons.arrow_back, 12 | 'arrow_forward': Icons.arrow_forward, 13 | 'build': Icons.build, 14 | 'call': Icons.call, 15 | 'camera_alt': Icons.camera_alt, 16 | 'check': Icons.check, 17 | 'close': Icons.close, 18 | 'delete': Icons.delete, 19 | 'done': Icons.done, 20 | 'edit': Icons.edit, 21 | 'favorite': Icons.favorite, 22 | 'home': Icons.home, 23 | 'info': Icons.info, 24 | 'menu': Icons.menu, 25 | 'more_vert': Icons.more_vert, 26 | 'notifications': Icons.notifications, 27 | 'phone': Icons.phone, 28 | 'search': Icons.search, 29 | 'settings': Icons.settings, 30 | 'share': Icons.share, 31 | 'shopping_cart': Icons.shopping_cart, 32 | 'star': Icons.star, 33 | 'accessibility': Icons.accessibility, 34 | 'accessible': Icons.accessible, 35 | 'account_circle': Icons.account_circle, 36 | 'account_box': Icons.account_box, 37 | 'add_box': Icons.add_box, 38 | 'add_circle': Icons.add_circle, 39 | 'add_shopping_cart': Icons.add_shopping_cart, 40 | 'airplanemode_active': Icons.airplanemode_active, 41 | 'album': Icons.album, 42 | 'all_inbox': Icons.all_inbox, 43 | 'analytics': Icons.analytics, 44 | 'archive': Icons.archive, 45 | 'arrow_drop_down': Icons.arrow_drop_down, 46 | 'arrow_drop_up': Icons.arrow_drop_up, 47 | 'assessment': Icons.assessment, 48 | 'attach_file': Icons.attach_file, 49 | 'attachment': Icons.attachment, 50 | 'auto_fix_high': Icons.auto_fix_high, 51 | 'backup': Icons.backup, 52 | 'battery_full': Icons.battery_full, 53 | 'bed': Icons.bed, 54 | 'book': Icons.book, 55 | 'bookmark': Icons.bookmark, 56 | 'brightness_4': Icons.brightness_4, 57 | 'business': Icons.business, 58 | 'calendar_today': Icons.calendar_today, 59 | 'camera': Icons.camera, 60 | 'card_giftcard': Icons.card_giftcard, 61 | 'card_membership': Icons.card_membership, 62 | 'chat': Icons.chat, 63 | 'check_box': Icons.check_box, 64 | 'chevron_left': Icons.chevron_left, 65 | 'chevron_right': Icons.chevron_right, 66 | 'cloud': Icons.cloud, 67 | 'cloud_queue': Icons.cloud_queue, 68 | 'code': Icons.code, 69 | 'commute': Icons.commute, 70 | 'compare_arrows': Icons.compare_arrows, 71 | 'credit_card': Icons.credit_card, 72 | 'crop_square': Icons.crop_square, 73 | 'dashboard': Icons.dashboard, 74 | 'directions': Icons.directions, 75 | 'directions_bike': Icons.directions_bike, 76 | 'directions_car': Icons.directions_car, 77 | 'directions_walk': Icons.directions_walk, 78 | 'download': Icons.download, 79 | 'event': Icons.event, 80 | 'exit_to_app': Icons.exit_to_app, 81 | 'face': Icons.face, 82 | 'fingerprint': Icons.fingerprint, 83 | }; 84 | 85 | for (final iconEntry in iconsMap.entries) { 86 | goldenTest( 87 | 'should render icon ${iconEntry.key} correctly', 88 | fileName: 'icons/${iconEntry.key}', 89 | skip: true, // removes if you want to check loading fonts performance 90 | builder: () => GoldenTestScenario.builder( 91 | name: iconEntry.key, 92 | builder: (_) => Icon( 93 | iconEntry.value, 94 | size: 64, 95 | ), 96 | ), 97 | ); 98 | } 99 | }); 100 | } 101 | -------------------------------------------------------------------------------- /example/windows/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.14) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") 12 | 13 | # === Flutter Library === 14 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") 15 | 16 | # Published to parent scope for install step. 17 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 18 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 19 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 20 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) 21 | 22 | list(APPEND FLUTTER_LIBRARY_HEADERS 23 | "flutter_export.h" 24 | "flutter_windows.h" 25 | "flutter_messenger.h" 26 | "flutter_plugin_registrar.h" 27 | "flutter_texture_registrar.h" 28 | ) 29 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") 30 | add_library(flutter INTERFACE) 31 | target_include_directories(flutter INTERFACE 32 | "${EPHEMERAL_DIR}" 33 | ) 34 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") 35 | add_dependencies(flutter flutter_assemble) 36 | 37 | # === Wrapper === 38 | list(APPEND CPP_WRAPPER_SOURCES_CORE 39 | "core_implementations.cc" 40 | "standard_codec.cc" 41 | ) 42 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") 43 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN 44 | "plugin_registrar.cc" 45 | ) 46 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") 47 | list(APPEND CPP_WRAPPER_SOURCES_APP 48 | "flutter_engine.cc" 49 | "flutter_view_controller.cc" 50 | ) 51 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") 52 | 53 | # Wrapper sources needed for a plugin. 54 | add_library(flutter_wrapper_plugin STATIC 55 | ${CPP_WRAPPER_SOURCES_CORE} 56 | ${CPP_WRAPPER_SOURCES_PLUGIN} 57 | ) 58 | apply_standard_settings(flutter_wrapper_plugin) 59 | set_target_properties(flutter_wrapper_plugin PROPERTIES 60 | POSITION_INDEPENDENT_CODE ON) 61 | set_target_properties(flutter_wrapper_plugin PROPERTIES 62 | CXX_VISIBILITY_PRESET hidden) 63 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) 64 | target_include_directories(flutter_wrapper_plugin PUBLIC 65 | "${WRAPPER_ROOT}/include" 66 | ) 67 | add_dependencies(flutter_wrapper_plugin flutter_assemble) 68 | 69 | # Wrapper sources needed for the runner. 70 | add_library(flutter_wrapper_app STATIC 71 | ${CPP_WRAPPER_SOURCES_CORE} 72 | ${CPP_WRAPPER_SOURCES_APP} 73 | ) 74 | apply_standard_settings(flutter_wrapper_app) 75 | target_link_libraries(flutter_wrapper_app PUBLIC flutter) 76 | target_include_directories(flutter_wrapper_app PUBLIC 77 | "${WRAPPER_ROOT}/include" 78 | ) 79 | add_dependencies(flutter_wrapper_app flutter_assemble) 80 | 81 | # === Flutter tool backend === 82 | # _phony_ is a non-existent file to force this command to run every time, 83 | # since currently there's no way to get a full input/output list from the 84 | # flutter tool. 85 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") 86 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) 87 | add_custom_command( 88 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 89 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} 90 | ${CPP_WRAPPER_SOURCES_APP} 91 | ${PHONY_OUTPUT} 92 | COMMAND ${CMAKE_COMMAND} -E env 93 | ${FLUTTER_TOOL_ENVIRONMENT} 94 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" 95 | windows-x64 $ 96 | VERBATIM 97 | ) 98 | add_custom_target(flutter_assemble DEPENDS 99 | "${FLUTTER_LIBRARY}" 100 | ${FLUTTER_LIBRARY_HEADERS} 101 | ${CPP_WRAPPER_SOURCES_CORE} 102 | ${CPP_WRAPPER_SOURCES_PLUGIN} 103 | ${CPP_WRAPPER_SOURCES_APP} 104 | ) 105 | -------------------------------------------------------------------------------- /example/linux/my_application.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | #include 4 | #ifdef GDK_WINDOWING_X11 5 | #include 6 | #endif 7 | 8 | #include "flutter/generated_plugin_registrant.h" 9 | 10 | struct _MyApplication { 11 | GtkApplication parent_instance; 12 | char** dart_entrypoint_arguments; 13 | }; 14 | 15 | G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) 16 | 17 | // Implements GApplication::activate. 18 | static void my_application_activate(GApplication* application) { 19 | MyApplication* self = MY_APPLICATION(application); 20 | GtkWindow* window = 21 | GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); 22 | 23 | // Use a header bar when running in GNOME as this is the common style used 24 | // by applications and is the setup most users will be using (e.g. Ubuntu 25 | // desktop). 26 | // If running on X and not using GNOME then just use a traditional title bar 27 | // in case the window manager does more exotic layout, e.g. tiling. 28 | // If running on Wayland assume the header bar will work (may need changing 29 | // if future cases occur). 30 | gboolean use_header_bar = TRUE; 31 | #ifdef GDK_WINDOWING_X11 32 | GdkScreen* screen = gtk_window_get_screen(window); 33 | if (GDK_IS_X11_SCREEN(screen)) { 34 | const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); 35 | if (g_strcmp0(wm_name, "GNOME Shell") != 0) { 36 | use_header_bar = FALSE; 37 | } 38 | } 39 | #endif 40 | if (use_header_bar) { 41 | GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); 42 | gtk_widget_show(GTK_WIDGET(header_bar)); 43 | gtk_header_bar_set_title(header_bar, "example"); 44 | gtk_header_bar_set_show_close_button(header_bar, TRUE); 45 | gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); 46 | } else { 47 | gtk_window_set_title(window, "example"); 48 | } 49 | 50 | gtk_window_set_default_size(window, 1280, 720); 51 | gtk_widget_show(GTK_WIDGET(window)); 52 | 53 | g_autoptr(FlDartProject) project = fl_dart_project_new(); 54 | fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); 55 | 56 | FlView* view = fl_view_new(project); 57 | gtk_widget_show(GTK_WIDGET(view)); 58 | gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); 59 | 60 | fl_register_plugins(FL_PLUGIN_REGISTRY(view)); 61 | 62 | gtk_widget_grab_focus(GTK_WIDGET(view)); 63 | } 64 | 65 | // Implements GApplication::local_command_line. 66 | static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { 67 | MyApplication* self = MY_APPLICATION(application); 68 | // Strip out the first argument as it is the binary name. 69 | self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); 70 | 71 | g_autoptr(GError) error = nullptr; 72 | if (!g_application_register(application, nullptr, &error)) { 73 | g_warning("Failed to register: %s", error->message); 74 | *exit_status = 1; 75 | return TRUE; 76 | } 77 | 78 | g_application_activate(application); 79 | *exit_status = 0; 80 | 81 | return TRUE; 82 | } 83 | 84 | // Implements GObject::dispose. 85 | static void my_application_dispose(GObject* object) { 86 | MyApplication* self = MY_APPLICATION(object); 87 | g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); 88 | G_OBJECT_CLASS(my_application_parent_class)->dispose(object); 89 | } 90 | 91 | static void my_application_class_init(MyApplicationClass* klass) { 92 | G_APPLICATION_CLASS(klass)->activate = my_application_activate; 93 | G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; 94 | G_OBJECT_CLASS(klass)->dispose = my_application_dispose; 95 | } 96 | 97 | static void my_application_init(MyApplication* self) {} 98 | 99 | MyApplication* my_application_new() { 100 | return MY_APPLICATION(g_object_new(my_application_get_type(), 101 | "application-id", APPLICATION_ID, 102 | "flags", G_APPLICATION_NON_UNIQUE, 103 | nullptr)); 104 | } 105 | -------------------------------------------------------------------------------- /example/windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project-level configuration. 2 | cmake_minimum_required(VERSION 3.14) 3 | project(example LANGUAGES CXX) 4 | 5 | # The name of the executable created for the application. Change this to change 6 | # the on-disk name of your application. 7 | set(BINARY_NAME "example") 8 | 9 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent 10 | # versions of CMake. 11 | cmake_policy(SET CMP0063 NEW) 12 | 13 | # Define build configuration option. 14 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 15 | if(IS_MULTICONFIG) 16 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" 17 | CACHE STRING "" FORCE) 18 | else() 19 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 20 | set(CMAKE_BUILD_TYPE "Debug" CACHE 21 | STRING "Flutter build mode" FORCE) 22 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 23 | "Debug" "Profile" "Release") 24 | endif() 25 | endif() 26 | # Define settings for the Profile build mode. 27 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") 28 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") 29 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") 30 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") 31 | 32 | # Use Unicode for all projects. 33 | add_definitions(-DUNICODE -D_UNICODE) 34 | 35 | # Compilation settings that should be applied to most targets. 36 | # 37 | # Be cautious about adding new options here, as plugins use this function by 38 | # default. In most cases, you should add new options to specific targets instead 39 | # of modifying this function. 40 | function(APPLY_STANDARD_SETTINGS TARGET) 41 | target_compile_features(${TARGET} PUBLIC cxx_std_17) 42 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") 43 | target_compile_options(${TARGET} PRIVATE /EHsc) 44 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") 45 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") 46 | endfunction() 47 | 48 | # Flutter library and tool build rules. 49 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 50 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 51 | 52 | # Application build; see runner/CMakeLists.txt. 53 | add_subdirectory("runner") 54 | 55 | # Generated plugin build rules, which manage building the plugins and adding 56 | # them to the application. 57 | include(flutter/generated_plugins.cmake) 58 | 59 | 60 | # === Installation === 61 | # Support files are copied into place next to the executable, so that it can 62 | # run in place. This is done instead of making a separate bundle (as on Linux) 63 | # so that building and running from within Visual Studio will work. 64 | set(BUILD_BUNDLE_DIR "$") 65 | # Make the "install" step default, as it's required to run. 66 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) 67 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 68 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 69 | endif() 70 | 71 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 72 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") 73 | 74 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 75 | COMPONENT Runtime) 76 | 77 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 78 | COMPONENT Runtime) 79 | 80 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 81 | COMPONENT Runtime) 82 | 83 | if(PLUGIN_BUNDLED_LIBRARIES) 84 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 85 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 86 | COMPONENT Runtime) 87 | endif() 88 | 89 | # Fully re-copy the assets directory on each build to avoid having stale files 90 | # from a previous install. 91 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 92 | install(CODE " 93 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 94 | " COMPONENT Runtime) 95 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 96 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 97 | 98 | # Install the AOT library on non-Debug builds only. 99 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 100 | CONFIGURATIONS Profile;Release 101 | COMPONENT Runtime) 102 | -------------------------------------------------------------------------------- /lib/src/golden_test_scenario.dart: -------------------------------------------------------------------------------- 1 | import 'package:alchemist/alchemist.dart'; 2 | import 'package:alchemist/src/golden_test_scenario_constraints.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | /// An internal [WidgetBuilder] that builds the widget it's given. 6 | WidgetBuilder _build(Widget build) => 7 | (context) => build; 8 | 9 | /// {@template golden_test_scenario} 10 | /// A widget that displays its child with a label for use in golden tests. 11 | /// 12 | /// This widget is used in tandem with [GoldenTestGroup] to display a set of 13 | /// golden test scenarios, which can be converted to snapshot files when used 14 | /// in [goldenTest]s. 15 | /// 16 | /// See also: 17 | /// * [goldenTest], which renders and compares [GoldenTestGroup]s. 18 | /// * [GoldenTestGroup], which groups multiple [GoldenTestScenario]s together. 19 | /// * [GoldenTestScenario.builder], which creates a test scenario from a 20 | /// [WidgetBuilder] that allows access to the [BuildContext] of the widget. 21 | /// * [GoldenTestScenario.withTextScaleFactor], which allows a default text 22 | /// scale factor to be applied to the child. 23 | /// {@endtemplate} 24 | class GoldenTestScenario extends StatelessWidget { 25 | /// {@macro golden_test_scenario} 26 | GoldenTestScenario({ 27 | required this.name, 28 | required Widget child, 29 | super.key, 30 | this.constraints, 31 | }) : builder = _build(child); 32 | 33 | /// Creates a [GoldenTestScenario] with a [builder] function that allows 34 | /// access to the [BuildContext] of the widget. 35 | const GoldenTestScenario.builder({ 36 | required this.name, 37 | required this.builder, 38 | super.key, 39 | this.constraints, 40 | }); 41 | 42 | /// Creates a [GoldenTestScenario] with a custom [textScaler] that 43 | /// applies a default scale of text to its child. 44 | GoldenTestScenario.withTextScaleFactor({ 45 | required this.name, 46 | required TextScaler textScaler, 47 | required Widget child, 48 | super.key, 49 | this.constraints, 50 | }) : builder = _build( 51 | _CustomTextScaleFactor(textScaler: textScaler, child: child), 52 | ); 53 | 54 | /// The name of the scenario. 55 | /// 56 | /// This text will be rendered as a [Text] label above the child, and is used 57 | /// to differentiate between different [GoldenTestScenario]s in the same 58 | /// [GoldenTestGroup]. 59 | /// 60 | /// This is required. 61 | final String name; 62 | 63 | /// The builder function that builds the widget to be displayed. 64 | final WidgetBuilder builder; 65 | 66 | /// Constraints to apply to the widget built by [builder] 67 | final BoxConstraints? constraints; 68 | 69 | @override 70 | Widget build(BuildContext context) { 71 | final testTheme = 72 | Theme.of(context).extension() ?? 73 | AlchemistConfig.current().goldenTestTheme ?? 74 | GoldenTestTheme.standard(); 75 | return Padding( 76 | padding: testTheme.padding, 77 | child: Column( 78 | crossAxisAlignment: CrossAxisAlignment.start, 79 | mainAxisSize: MainAxisSize.min, 80 | children: [ 81 | Text( 82 | name, 83 | style: testTheme.nameTextStyle, 84 | textHeightBehavior: const TextHeightBehavior( 85 | applyHeightToFirstAscent: false, 86 | ), 87 | ), 88 | const SizedBox(height: 8), 89 | ConstrainedBox( 90 | constraints: 91 | constraints ?? 92 | GoldenTestScenarioConstraints.maybeOf(context) ?? 93 | const BoxConstraints(), 94 | child: Builder(builder: builder), 95 | ), 96 | ], 97 | ), 98 | ); 99 | } 100 | } 101 | 102 | /// {@template _custom_text_scale_factor} 103 | /// An internal widget used to apply a default [textScaler] to its [child]. 104 | /// {@endtemplate} 105 | @protected 106 | class _CustomTextScaleFactor extends StatelessWidget { 107 | /// {@macro _custom_text_scale_factor} 108 | const _CustomTextScaleFactor({required this.textScaler, required this.child}); 109 | 110 | /// The TextScaler will be applied to the [child]. 111 | final TextScaler textScaler; 112 | 113 | /// The child widget to apply the [textScaler] to. 114 | final Widget child; 115 | 116 | @override 117 | Widget build(BuildContext context) { 118 | return MediaQuery( 119 | data: MediaQuery.of(context).copyWith(textScaler: textScaler), 120 | child: child, 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /test/flutter_test_config.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | 5 | import 'package:alchemist/alchemist.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_test/flutter_test.dart'; 8 | import 'package:path/path.dart' as path; 9 | import 'package:version/version.dart'; 10 | 11 | Future testExecutable(FutureOr Function() testMain) async { 12 | final runningOnCi = Platform.environment.containsKey('GITHUB_ACTIONS'); 13 | 14 | // Grab the flutter version from the current environment. 15 | final versionResult = await Process.run('flutter', [ 16 | '--version', 17 | '--machine', 18 | ]); 19 | 20 | if (versionResult.exitCode != 0) { 21 | throw const ProcessException('flutter', [ 22 | '--version', 23 | '--machine', 24 | ], 'Failed to get flutter version'); 25 | } 26 | 27 | final versionJson = versionResult.stdout.toString().trim(); 28 | final flutterData = json.decode(versionJson) as Map; 29 | final version = flutterData['flutterVersion'] as String; 30 | 31 | if (!version.isValidVersion()) { 32 | throw ProcessException( 33 | 'flutter', 34 | ['--version', '--machine'], 35 | 'Invalid flutter version returned by `flutter version`: $version', 36 | ); 37 | } 38 | 39 | final parsedVersion = Version.parse(version); 40 | final subDirectories = Directory( 41 | path.join(Directory.current.path, 'test', 'smoke_tests', 'goldens'), 42 | ).listSync().whereType().toList(); 43 | 44 | final candidates = subDirectories.where((dir) { 45 | try { 46 | final dirVersion = Version.parse(path.basename(dir.path)); 47 | return dirVersion <= parsedVersion; 48 | } on FormatException { 49 | return false; 50 | } 51 | }); 52 | 53 | /// Returns the goldens directory for the provided flutter version. 54 | /// 55 | /// In order, this attempts to find: 56 | /// - A directory that matches the provided flutter version exactly. 57 | /// - If no exact match is found, the highest version directory that is less 58 | /// than or equal to the provided flutter version. 59 | /// - If no directories are found, an error is thrown. 60 | /// 61 | /// If `autoUpdateGoldenFiles` is true (meaning we've passed 62 | /// --update-goldens), a directory matching the provided flutter version will 63 | /// be created if it does not already exist. 64 | /// 65 | /// Doing this supports running our smoke tests against specific versions of 66 | /// Flutter, allowing us to maintain goldens for each version when necessary. 67 | Directory goldensDirectory() { 68 | // If we're updating golden files, always return the associated directory. 69 | if (autoUpdateGoldenFiles) { 70 | return Directory(path.join('goldens', parsedVersion.toString())); 71 | } 72 | 73 | if (candidates.isEmpty) { 74 | throw ArgumentError( 75 | 'No valid directories found in `goldens` for the current ' 76 | 'flutter version: $parsedVersion', 77 | ); 78 | } 79 | 80 | // If we have multiple candidates, we want to find the highest version 81 | // directory that is less than or equal to the provided flutter version. 82 | return candidates.reduce((a, b) { 83 | final aVersion = Version.parse(path.basename(a.path)); 84 | final bVersion = Version.parse(path.basename(b.path)); 85 | return aVersion > bVersion ? a : b; 86 | }); 87 | } 88 | 89 | final directory = goldensDirectory(); 90 | 91 | Future filePathResolver( 92 | String fileName, 93 | String environmentName, 94 | ) async { 95 | return path.join( 96 | directory.path, 97 | environmentName.toLowerCase(), 98 | '$fileName.png', 99 | ); 100 | } 101 | 102 | return AlchemistConfig.runWithConfig( 103 | config: AlchemistConfig( 104 | theme: ThemeData( 105 | useMaterial3: false, 106 | textTheme: const TextTheme().apply(fontFamily: 'Roboto'), 107 | ), 108 | ciGoldensConfig: AlchemistConfig.current() 109 | .ciGoldensConfig // 110 | .copyWith(filePathResolver: filePathResolver, enabled: runningOnCi), 111 | platformGoldensConfig: AlchemistConfig.current() 112 | .platformGoldensConfig // 113 | .copyWith(filePathResolver: filePathResolver, enabled: !runningOnCi), 114 | ), 115 | run: testMain, 116 | ); 117 | } 118 | 119 | extension on String { 120 | bool isValidVersion() { 121 | try { 122 | Version.parse(this); 123 | return true; 124 | } on FormatException { 125 | return false; 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /.github/workflows/check_compat.yaml: -------------------------------------------------------------------------------- 1 | name: Compatibility Check 2 | 3 | # ignore: RiskyTriggers 4 | on: 5 | schedule: 6 | # Run daily at 00:00 UTC 7 | - cron: "0 0 * * *" 8 | workflow_dispatch: 9 | inputs: 10 | version_override: 11 | description: "Override the Flutter version to test against" 12 | required: false 13 | default: "" 14 | type: string 15 | 16 | jobs: 17 | build_with_matrix: 18 | if: ${{ github.event_name == 'schedule' || github.event.inputs.version_override == '' }} 19 | runs-on: ubuntu-latest 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | channel: ["stable", "beta"] 24 | 25 | steps: 26 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 27 | 28 | - uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0 29 | with: 30 | channel: ${{ matrix.channel }} 31 | cache: true 32 | 33 | - name: Install Dependencies 34 | run: flutter packages get 35 | 36 | - name: Disable animations 37 | run: flutter config --no-cli-animations 38 | 39 | - name: Run tests 40 | run: | 41 | flutter test --no-pub --coverage --test-randomize-ordering-seed=random 42 | 43 | - name: Upload failures 44 | if: failure() 45 | uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 46 | with: 47 | name: "golden_failures_${{ matrix.channel }}" 48 | path: | 49 | **/failures/**/*.png 50 | 51 | - name: Create job URL 52 | if: failure() 53 | id: create-job-url 54 | run: | 55 | echo "job_url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> "$GITHUB_OUTPUT" 56 | 57 | - name: Notify failure 58 | if: failure() 59 | uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52 # v2.1.0 60 | with: 61 | webhook-type: webhook-trigger 62 | webhook: ${{ secrets.MOBILE_OSS_SLACK_WEBHOOK }} 63 | payload: | 64 | text: "Alchemist smoke tests failed on the ${{ matrix.channel }} channel. Check them out and determine root cause: ${{ steps.create-job-url.outputs.job_url }}" 65 | blocks: 66 | - type: "section" 67 | text: 68 | type: "mrkdwn" 69 | text: "Alchemist smoke tests failed on the `${{ matrix.channel }}` channel. Check them out and determine root cause. <${{ steps.create-job-url.outputs.job_url }}|Job URL>" 70 | 71 | build_with_override: 72 | if: ${{ github.event.inputs.version_override != '' }} 73 | runs-on: ubuntu-latest 74 | 75 | steps: 76 | - name: Validate version format 77 | env: 78 | VERSION: ${{ github.event.inputs.version_override }} 79 | run: | 80 | # Validate semver format including pre-release versions like 3.37.0-0.1.pre 81 | # This regex matches standard semver including pre-release and build metadata 82 | if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z\-\.]+)?(\+[0-9A-Za-z\-\.]+)?$'; then 83 | echo "Error: Invalid version format. Version must be a valid semver string (e.g., 3.37.0 or 3.37.0-0.1.pre)" 84 | echo "Provided version: $VERSION" 85 | exit 1 86 | fi 87 | echo "✓ Version format validated: $VERSION" 88 | 89 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 90 | 91 | - uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0 92 | with: 93 | flutter-version: ${{ github.event.inputs.version_override }} 94 | channel: any 95 | cache: true 96 | 97 | - name: Install Dependencies 98 | run: flutter packages get 99 | 100 | - name: Disable animations 101 | run: flutter config --no-cli-animations 102 | 103 | - name: Run tests 104 | run: | 105 | flutter test --no-pub --coverage --test-randomize-ordering-seed=random 106 | 107 | - name: Upload failures 108 | if: failure() 109 | uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 110 | with: 111 | name: "golden_failures_${{ github.event.inputs.version_override }}" 112 | path: | 113 | **/failures/**/*.png 114 | 115 | - name: Create job URL 116 | if: failure() 117 | id: create-job-url 118 | run: | 119 | echo "job_url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> "$GITHUB_OUTPUT" 120 | -------------------------------------------------------------------------------- /test/src/golden_test_scenario_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:alchemist/src/golden_test_scenario.dart'; 2 | import 'package:alchemist/src/golden_test_scenario_constraints.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | void main() { 7 | group('GoldenTestScenario', () { 8 | Widget buildSubject({ 9 | Key? key, 10 | String name = 'name', 11 | Widget child = const Text('child'), 12 | BoxConstraints? constraints, 13 | }) { 14 | return MaterialApp( 15 | home: Scaffold( 16 | body: GoldenTestScenario( 17 | key: key, 18 | name: name, 19 | constraints: constraints, 20 | child: child, 21 | ), 22 | ), 23 | ); 24 | } 25 | 26 | testWidgets('renders name as label', (tester) async { 27 | final subject = buildSubject(); 28 | 29 | await tester.pumpWidget(subject); 30 | 31 | expect(find.text('name'), findsOneWidget); 32 | }); 33 | 34 | testWidgets('renders child', (tester) async { 35 | final subject = buildSubject(); 36 | 37 | await tester.pumpWidget(subject); 38 | 39 | expect(find.text('child'), findsOneWidget); 40 | }); 41 | 42 | group('constraints', () { 43 | testWidgets('when null defaults to inherited constraints', ( 44 | tester, 45 | ) async { 46 | const constraints = BoxConstraints(maxWidth: 400); 47 | final subject = buildSubject(); 48 | 49 | await tester.pumpWidget( 50 | GoldenTestScenarioConstraints( 51 | constraints: constraints, 52 | child: subject, 53 | ), 54 | ); 55 | 56 | final findConstraints = find.ancestor( 57 | of: find.text('child'), 58 | matching: find.byWidgetPredicate( 59 | (widget) => 60 | widget is ConstrainedBox && widget.constraints == constraints, 61 | ), 62 | ); 63 | 64 | expect(findConstraints, findsOneWidget); 65 | }); 66 | 67 | testWidgets('constrains the child', (tester) async { 68 | const constraints = BoxConstraints(maxWidth: 400); 69 | final subject = buildSubject(constraints: constraints); 70 | 71 | await tester.pumpWidget(subject); 72 | 73 | final findConstraints = find.ancestor( 74 | of: find.text('child'), 75 | matching: find.byWidgetPredicate( 76 | (widget) => 77 | widget is ConstrainedBox && widget.constraints == constraints, 78 | ), 79 | ); 80 | 81 | expect(findConstraints, findsOneWidget); 82 | }); 83 | 84 | testWidgets('takes precedence over inherited constraints', ( 85 | tester, 86 | ) async { 87 | const constraints = BoxConstraints(maxWidth: 400); 88 | final subject = buildSubject(constraints: constraints); 89 | 90 | await tester.pumpWidget( 91 | GoldenTestScenarioConstraints( 92 | constraints: const BoxConstraints(maxHeight: 400), 93 | child: subject, 94 | ), 95 | ); 96 | 97 | final findConstraints = find.ancestor( 98 | of: find.text('child'), 99 | matching: find.byWidgetPredicate( 100 | (widget) => 101 | widget is ConstrainedBox && widget.constraints == constraints, 102 | ), 103 | ); 104 | 105 | expect(findConstraints, findsOneWidget); 106 | }); 107 | }); 108 | 109 | testWidgets('.builder constructor provides builder', (tester) async { 110 | Object? providedContext; 111 | 112 | final subject = GoldenTestScenario.builder( 113 | name: 'name', 114 | builder: (context) { 115 | providedContext = context; 116 | return const Text('child'); 117 | }, 118 | ); 119 | 120 | await tester.pumpWidget(MaterialApp(home: Scaffold(body: subject))); 121 | 122 | expect(providedContext, isNotNull); 123 | expect(providedContext, isA()); 124 | }); 125 | 126 | testWidgets('.withTextScaleFactor sets correct default textScaler', ( 127 | tester, 128 | ) async { 129 | const textScaler = TextScaler.linear(2); 130 | final subject = GoldenTestScenario.withTextScaleFactor( 131 | textScaler: textScaler, 132 | name: 'name', 133 | child: const Text('child'), 134 | ); 135 | 136 | await tester.pumpWidget(MaterialApp(home: Scaffold(body: subject))); 137 | 138 | final element = tester.element(find.text('child')); 139 | final mediaQuery = MediaQuery.maybeOf(element); 140 | 141 | expect(mediaQuery, isNotNull); 142 | expect( 143 | mediaQuery, 144 | isA().having( 145 | (m) => m.textScaler, 146 | 'textScaler', 147 | textScaler, 148 | ), 149 | ); 150 | }); 151 | }); 152 | } 153 | -------------------------------------------------------------------------------- /lib/src/golden_test_runner.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui' as ui; 2 | 3 | import 'package:alchemist/src/golden_test_adapter.dart'; 4 | import 'package:alchemist/src/golden_test_theme.dart'; 5 | import 'package:alchemist/src/interactions.dart'; 6 | import 'package:alchemist/src/pumps.dart'; 7 | import 'package:flutter/foundation.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | /// Default golden test adapter used to interface with Flutter's testing 12 | /// framework. 13 | GoldenTestAdapter defaultGoldenTestAdapter = const FlutterGoldenTestAdapter(); 14 | GoldenTestAdapter _goldenTestAdapter = defaultGoldenTestAdapter; 15 | 16 | /// Golden test adapter used to interface with Flutter's test framework. 17 | /// Overriding this makes it easier to unit-test Alchemist. 18 | GoldenTestAdapter get goldenTestAdapter => _goldenTestAdapter; 19 | set goldenTestAdapter(GoldenTestAdapter value) => _goldenTestAdapter = value; 20 | 21 | /// {@template golden_test_runner} 22 | /// A utility class for running an individual golden test. 23 | /// {@endtemplate} 24 | // ignore: one_member_abstracts 25 | abstract class GoldenTestRunner { 26 | /// {@macro golden_test_runner} 27 | const GoldenTestRunner(); 28 | 29 | /// Runs a single golden test expectation. 30 | Future run({ 31 | required WidgetTester tester, 32 | required Object goldenPath, 33 | required Widget widget, 34 | required ThemeData? globalConfigTheme, 35 | required ThemeData? variantConfigTheme, 36 | required GoldenTestTheme? goldenTestTheme, 37 | bool forceUpdate = false, 38 | bool obscureText = false, 39 | bool renderShadows = false, 40 | double textScaleFactor = 1.0, 41 | BoxConstraints constraints = const BoxConstraints(), 42 | PumpAction pumpBeforeTest = onlyPumpAndSettle, 43 | PumpWidget pumpWidget = onlyPumpWidget, 44 | Interaction? whilePerforming, 45 | }); 46 | } 47 | 48 | /// {@template flutter_golden_test_runner} 49 | /// A [GoldenTestRunner] which uses the Flutter test framework to execute 50 | /// a golden test. 51 | /// {@endtemplate} 52 | class FlutterGoldenTestRunner extends GoldenTestRunner { 53 | /// {@macro flutter_golden_test_runner} 54 | const FlutterGoldenTestRunner() : super(); 55 | 56 | @override 57 | Future run({ 58 | required WidgetTester tester, 59 | required Object goldenPath, 60 | required Widget widget, 61 | ThemeData? globalConfigTheme, 62 | ThemeData? variantConfigTheme, 63 | GoldenTestTheme? goldenTestTheme, 64 | bool forceUpdate = false, 65 | bool obscureText = false, 66 | bool renderShadows = false, 67 | double textScaleFactor = 1.0, 68 | BoxConstraints constraints = const BoxConstraints(), 69 | PumpAction pumpBeforeTest = onlyPumpAndSettle, 70 | PumpWidget pumpWidget = onlyPumpWidget, 71 | Interaction? whilePerforming, 72 | }) async { 73 | assert( 74 | goldenPath is String || goldenPath is Uri, 75 | 'Golden path must be a String or Uri.', 76 | ); 77 | 78 | final rootKey = FlutterGoldenTestAdapter.rootKey; 79 | 80 | final mementoDebugDisableShadows = debugDisableShadows; 81 | debugDisableShadows = !renderShadows; 82 | 83 | Future? imageFuture; 84 | try { 85 | await goldenTestAdapter.pumpGoldenTest( 86 | tester: tester, 87 | rootKey: rootKey, 88 | textScaleFactor: textScaleFactor, 89 | constraints: constraints, 90 | obscureFont: obscureText, 91 | globalConfigTheme: globalConfigTheme, 92 | variantConfigTheme: variantConfigTheme, 93 | goldenTestTheme: goldenTestTheme, 94 | pumpBeforeTest: pumpBeforeTest, 95 | pumpWidget: pumpWidget, 96 | widget: widget, 97 | ); 98 | 99 | AsyncCallback? cleanup; 100 | if (whilePerforming != null) { 101 | cleanup = await whilePerforming(tester); 102 | } 103 | 104 | final finder = find.byKey(rootKey); 105 | 106 | if (obscureText) { 107 | imageFuture = goldenTestAdapter.getBlockedTextImage( 108 | finder: finder, 109 | tester: tester, 110 | ); 111 | } 112 | 113 | final toMatch = imageFuture ?? finder; 114 | 115 | try { 116 | await goldenTestAdapter.withForceUpdateGoldenFiles( 117 | forceUpdate: forceUpdate, 118 | callback: goldenTestAdapter.goldenFileExpectation( 119 | toMatch, 120 | goldenPath, 121 | ), 122 | ); 123 | await cleanup?.call(); 124 | } on TestFailure { 125 | rethrow; 126 | } 127 | } finally { 128 | debugDisableShadows = mementoDebugDisableShadows; 129 | final image = await imageFuture; 130 | image?.dispose(); 131 | 132 | await tester.binding.setSurfaceSize(null); 133 | tester.view.resetPhysicalSize(); 134 | addTearDown(() async { 135 | tester.view.resetDevicePixelRatio(); 136 | }); 137 | } 138 | } 139 | } 140 | --------------------------------------------------------------------------------