From 5303eac1e5147d6c58251d7d5ea25a360d29d203 Mon Sep 17 00:00:00 2001 From: Dmitry Repin Date: Mon, 1 May 2023 20:17:55 +0400 Subject: [PATCH] tour of beam integration tests (#25925) * Integration test to load the default example of the default SDK and change the example (#24730) (#24729) * Fix formatting and README (#24730) * Support collection v1.17.0 (#24730) * LoadingIndicator on chaning examples, remove duplicating licenses (#24730) * Add a missing license header (#24730) * Integration test for changing SDK and running code (#24779) (#382) * Integration test for changing SDK and running code (#24779) * Rename an integration test (#24779) * Use enum to switch SDK in integration test (#24779) * Find SDK in a dropdown by key (#24779) * Add a TODO (#24779) * Fix exports (#24779) * Issue24779 integration changing sdk from 24370 (#387) * Integration test for changing SDK and running code (#24779) * Rename an integration test (#24779) * Use enum to switch SDK in integration test (#24779) * Find SDK in a dropdown by key (#24779) * Add a TODO (#24779) * Fix exports (#24779) * Integration tests miscellaneous UI (#383) * miscellaneous ui integration tests * reverted pubspec.lock * gradle tasks ordered alhpabetically * integration tests refactoring * clean code * integration tests miscellaneous ui fix pr * rename method * added layout adaptivity * A minor cleanup (#24779) Co-authored-by: Dmitry Repin * integration tests run and editing * example selector test * minor fixes * rat * fix pr * minor * minor * rat * integration test finder written * integration test minor fixes * minor fixes * removed comment * minor fixes * playground integration tests minor fixes * integration test pumpAnSettleNoException * integration test shortcut refactor * integration test another changing shortcuts running * upgrade to flutter 3.7.1 * workaround comment * playground frontend updated major versions * issues 25329 25331 25336 * 25329 extract connectivity extension to separate file * Upgrade Flutter to 3.7.3 in integration tests (#24730) * Fix integration test (#24730) * fix cors issue and added mouse scroll to tags * Upgrade Flutter in Dockerfile (#24720) * sorting moved to model * sorting moved to model * sorting moved to model * bugs fix * issue 25278 * fix pr * quites fix in en.yaml * Fix not loading default example (#25528) * fix compile error * Refactor output tabs, test embedded playground (#25136) (#439) * Refactor output tabs, test embedded playground (#25136) * Clean up (#25136) * Change example paths to IDs in integration tests * issue25640 tob ci * fix tob ci * rename ci process * test add new line to main * test add new line to main * commented unit test run * issue25640 changed server path * issue25640 tests on welcome page * deleted config.g.dart * issue25640 pr fixes * Update .github/workflows/tour_of_beam_frontend_test.yml Co-authored-by: alexeyinkin * Update learning/tour-of-beam/frontend/integration_test/welcome_page_test.dart Co-authored-by: alexeyinkin * Improve tests (#25640) * issue25640 tour page tests * pr fix * removed import * pr fix * fix test * 25640 fixed pubspec.lock * issue25640 fix readme * updated readme * issue25640 fixed after master merge * issue25483 ToB pipeline options * removed unnecesary variable * pr fix * Update learning/tour-of-beam/frontend/assets/translations/en.yaml Co-authored-by: alexeyinkin * playground hides when snippet does not exists * pipeline options extracted to playground components * issue25483 pipeline options * added errors handling, fix pr * refactoring * Revert "refactoring" This reverts commit 1540961fe7b66673f70f56632947cdd988c3a0b7. * removed unnecessary constants * playground controller in tour notifier becomes nullable * playground controller returned to non nullable in tour notifier * playground controller actions * removed unnecessary code * tob scaffold wrapped with animated builder * minor fixes * partially fixed tests * Upgrade flutter_code_editor to v0.2.19 (#25640) * Replace output SelectableText with a CodeField instance (#25640) * Trigger ToB integration tests (#25640) * Clean up (#25640) * Enable manual workflow runs for Playground and ToB integration tests (#25640) --------- Co-authored-by: Alexey Inkin Co-authored-by: alexeyinkin --- .../workflows/playground_frontend_test.yml | 6 +- .../workflows/tour_of_beam_frontend_test.yml | 70 +++++ learning/tour-of-beam/frontend/README.md | 37 ++- .../frontend/assets/translations/en.yaml | 4 + .../tour-of-beam/frontend/build.gradle.kts | 33 +++ .../{app_test.dart => common/common.dart} | 29 +- .../common/common_finders.dart | 22 +- .../integration_test/tour_page_test.dart | 259 ++++++++++++++++++ .../integration_test/welcome_page_test.dart | 120 ++++++++ .../frontend/lib/cache/unit_content.dart | 2 +- .../frontend/lib/components/scaffold.dart | 56 +++- learning/tour-of-beam/frontend/lib/main.dart | 2 +- .../frontend/lib/pages/tour/screen.dart | 60 +++- .../frontend/lib/pages/tour/state.dart | 37 ++- .../lib/pages/tour/widgets/content_tree.dart | 2 +- .../lib/pages/tour/widgets/group.dart | 1 + .../lib/pages/tour/widgets/module.dart | 4 +- .../frontend/lib/pages/tour/widgets/node.dart | 2 + .../pages/tour/widgets/pipeline_options.dart | 41 +++ .../lib/pages/tour/widgets/playground.dart | 22 +- .../frontend/lib/pages/tour/widgets/unit.dart | 3 + .../lib/pages/tour/widgets/unit_content.dart | 2 +- .../frontend/lib/pages/welcome/screen.dart | 4 +- .../frontend/lib/router/page_factory.dart | 1 + learning/tour-of-beam/frontend/pubspec.lock | 11 +- learning/tour-of-beam/frontend/pubspec.yaml | 1 + playground/frontend/README.md | 3 - .../integration_test/common/common.dart | 7 - .../common/common_finders.dart | 3 - .../integration_test/embedded_run_test.dart | 1 - ...tandalone_cancel_running_example_test.dart | 2 +- ...tandalone_change_example_sdk_run_test.dart | 4 +- ..._change_pipeline_options_and_run_test.dart | 5 +- .../standalone_miscellaneous_ui_test.dart | 2 +- playground/frontend/lib/constants/sizes.dart | 4 - playground/frontend/lib/l10n/app_en.arb | 20 -- .../share_dropdown/share_button.dart | 1 - .../modules/examples/example_selector.dart | 1 - .../modules/sdk/components/sdk_selector.dart | 1 - .../pages/standalone_playground/screen.dart | 1 - .../widgets/editor_textarea_wrapper.dart | 1 + .../assets/translations/en.yaml | 12 + .../lib/playground_components.dart | 9 + .../lib/src/constants/sizes.dart | 13 + .../lib/src/util}/dropdown_utils.dart | 3 +- .../lib/src/widgets/drag_handle.dart | 1 + .../dropdown_button/dropdown_button.dart | 38 +-- .../widgets/output/result_tab_content.dart | 89 ++++-- .../pipeline_option_controller.dart | 0 .../pipeline_option_label.dart | 6 +- .../pipeline_options_dropdown.dart | 14 +- .../pipeline_options_dropdown_body.dart | 47 ++-- .../pipeline_options_dropdown_input.dart | 14 +- .../pipeline_options_form.dart | 32 ++- .../pipeline_options_row.dart | 28 +- .../pipeline_options_text_field.dart | 19 +- .../lib/src/widgets/split_view.dart | 7 +- .../playground_components/pubspec.yaml | 2 +- .../lib/playground_components_dev.dart | 2 + .../lib/src/common_finders.dart | 25 +- .../toggle_brightness_mode_test.dart | 4 +- .../lib/src/expect.dart | 37 ++- .../lib/src/widget_tester.dart | 12 +- .../playground_components_dev/pubspec.yaml | 2 +- playground/frontend/pubspec.lock | 4 +- playground/frontend/pubspec.yaml | 2 +- settings.gradle.kts | 3 + 67 files changed, 1020 insertions(+), 292 deletions(-) create mode 100644 .github/workflows/tour_of_beam_frontend_test.yml rename learning/tour-of-beam/frontend/integration_test/{app_test.dart => common/common.dart} (54%) rename playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_separator.dart => learning/tour-of-beam/frontend/integration_test/common/common_finders.dart (64%) create mode 100644 learning/tour-of-beam/frontend/integration_test/tour_page_test.dart create mode 100644 learning/tour-of-beam/frontend/integration_test/welcome_page_test.dart create mode 100644 learning/tour-of-beam/frontend/lib/pages/tour/widgets/pipeline_options.dart rename playground/frontend/{lib/utils => playground_components/lib/src/util}/dropdown_utils.dart (95%) rename playground/frontend/{lib/components => playground_components/lib/src/widgets}/dropdown_button/dropdown_button.dart (84%) rename playground/frontend/{lib/modules/editor/components => playground_components/lib/src/widgets}/pipeline_options_dropdown/pipeline_option_controller.dart (100%) rename playground/frontend/{lib/modules/editor/components => playground_components/lib/src/widgets}/pipeline_options_dropdown/pipeline_option_label.dart (79%) rename playground/frontend/{lib/modules/editor/components => playground_components/lib/src/widgets}/pipeline_options_dropdown/pipeline_options_dropdown.dart (78%) rename playground/frontend/{lib/modules/editor/components => playground_components/lib/src/widgets}/pipeline_options_dropdown/pipeline_options_dropdown_body.dart (80%) rename playground/frontend/{lib/modules/editor/components => playground_components/lib/src/widgets}/pipeline_options_dropdown/pipeline_options_dropdown_input.dart (76%) rename playground/frontend/{lib/modules/editor/components => playground_components/lib/src/widgets}/pipeline_options_dropdown/pipeline_options_form.dart (65%) rename playground/frontend/{lib/modules/editor/components => playground_components/lib/src/widgets}/pipeline_options_dropdown/pipeline_options_row.dart (69%) rename playground/frontend/{lib/modules/editor/components => playground_components/lib/src/widgets}/pipeline_options_dropdown/pipeline_options_text_field.dart (81%) rename playground/frontend/{integration_test/miscellaneous_ui => playground_components_dev/lib/src/common_tests}/toggle_brightness_mode_test.dart (95%) diff --git a/.github/workflows/playground_frontend_test.yml b/.github/workflows/playground_frontend_test.yml index aefe2b7aa4d3..5407f45e812f 100644 --- a/.github/workflows/playground_frontend_test.yml +++ b/.github/workflows/playground_frontend_test.yml @@ -17,10 +17,12 @@ name: Playground Frontend Test on: push: - paths: ['playground/frontend/**'] + paths: + - 'playground/frontend/**' branches: ['**'] pull_request: - paths: ['playground/frontend/**'] + paths: + - 'playground/frontend/**' branches: ['**'] workflow_dispatch: diff --git a/.github/workflows/tour_of_beam_frontend_test.yml b/.github/workflows/tour_of_beam_frontend_test.yml new file mode 100644 index 000000000000..d4c0dbe41adc --- /dev/null +++ b/.github/workflows/tour_of_beam_frontend_test.yml @@ -0,0 +1,70 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Tour Of Beam Frontend Test + +on: + push: + paths: + - 'learning/tour-of-beam/frontend/**' + - 'playground/frontend/playground_components' + branches: ['**'] + pull_request: + paths: + - 'learning/tour-of-beam/frontend/**' + - 'playground/frontend/playground_components' + branches: ['**'] + workflow_dispatch: + +# This allows a subsequently queued workflow run to interrupt previous runs +concurrency: + group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' + cancel-in-progress: true + +jobs: + tour_of_beam_test: + name: Tour of Beam Test + runs-on: ubuntu-latest + + env: + FLUTTER_VERSION: '3.7.3' + + steps: + - uses: actions/checkout@v3 + + - name: 'Cache Flutter Dependencies' + uses: actions/cache@v3 + with: + path: /opt/hostedtoolcache/flutter + key: ${{ runner.OS }}-flutter-install-cache-${{ env.FLUTTER_VERSION }} + restore-keys: ${{ runner.OS }}-flutter-install-cache + + - uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + channel: 'stable' + + - name: 'Install Dependencies' + run: | + cd playground/frontend/playground_components && flutter pub get && cd - + cd playground/frontend/playground_components_dev && flutter pub get && cd - + cd learning/tour-of-beam/frontend && flutter pub get && cd - + + - uses: nanasess/setup-chromedriver@v1 + + - name: 'Integration tests' + run: | + chromedriver --port=4444 & + ./gradlew :learning:tour-of-beam:frontend:integrationTest -PdeviceId=web-server diff --git a/learning/tour-of-beam/frontend/README.md b/learning/tour-of-beam/frontend/README.md index cba80719dfec..177a8d816e18 100644 --- a/learning/tour-of-beam/frontend/README.md +++ b/learning/tour-of-beam/frontend/README.md @@ -60,14 +60,35 @@ To change the Google Project that is used as the backend: # Deployment -# Tests - -Install ChromeDriver to run integration tests in a browser: https://docs.flutter.dev/testing/integration-tests#running-in-a-browser -Run integration tests: -flutter drive \ - --driver=test_driver/integration_test.dart \ - --target=integration_test/counter_test.dart \ - -d web-server +# Integration Tests + +## Prerequisites + +1. Install Google Chrome: https://www.google.com/chrome/ +2. Install Chrome Driver: https://chromedriver.chromium.org/downloads + - Note: This GitHub action installs both Chrome and Chrome Driver: + https://github.com/nanasess/setup-chromedriver/blob/a249caaaad10fd12103028fd509853c2229eb6e6/lib/setup-chromedriver.sh +3. Retrieve the required dependencies for each project subdirectory by running the following commands: + +```bash +cd playground/frontend/playground_components && flutter pub get && cd - +cd playground/frontend/playground_components_dev && flutter pub get && cd - +cd learning/tour-of-beam/frontend && flutter pub get && cd - +``` + +## Running Tests + +1. Run the Chrome Driver on port 4444: `chromedriver --port=4444` +2. Run the integration tests: + +```bash +# To run in a visible Chrome window: +./gradlew :learning:tour-of-beam:frontend:integrationTest + +# Headless run without a browser window: +./gradlew :learning:tour-of-beam:frontend:integrationTest -PdeviceId=web-server +``` + # Packages diff --git a/learning/tour-of-beam/frontend/assets/translations/en.yaml b/learning/tour-of-beam/frontend/assets/translations/en.yaml index 380bb0ee4a5a..fdaf4525450f 100644 --- a/learning/tour-of-beam/frontend/assets/translations/en.yaml +++ b/learning/tour-of-beam/frontend/assets/translations/en.yaml @@ -31,6 +31,9 @@ ui: signOut: Sign out toWebsite: To Apache Beam website +errors: + toastTitle: Error + pages: welcome: ifSaveProgress: Your journey is broken down into learning modules. If you would like to save your progress and track completed modules, please @@ -51,6 +54,7 @@ pages: playground: Playground saving: Saving... summaryTitle: Table of Contents + pipelineOptionsButtonTitle: 'Pipeline Options ({count})' dialogs: signInIf: If you would like to save your progress and track completed modules diff --git a/learning/tour-of-beam/frontend/build.gradle.kts b/learning/tour-of-beam/frontend/build.gradle.kts index 31bf46e59ffe..2a70778bf08a 100644 --- a/learning/tour-of-beam/frontend/build.gradle.kts +++ b/learning/tour-of-beam/frontend/build.gradle.kts @@ -154,3 +154,36 @@ val deleteFilesByRegExp: (String) -> Unit = { re -> args("assets", "lib", "test", "-regex", re, "-delete") } } + +tasks.register("integrationTest") { + dependsOn("integrationTest_tour_page") + dependsOn("integrationTest_welcome_page") +} + +tasks.register("integrationTest_tour_page") { + doLast { + runIntegrationTest("tour_page", "/") + } +} + +tasks.register("integrationTest_welcome_page") { + doLast { + runIntegrationTest("welcome_page", "/") + } +} + +fun runIntegrationTest(path: String, url: String) { + // Run with -PdeviceId=web-server for headless tests. + val deviceId: String = if (project.hasProperty("deviceId")) project.property("deviceId") as String else "chrome" + + exec { + executable = "flutter" + args( + "drive", + "--driver=test_driver/integration_test.dart", + "--target=integration_test/${path}_test.dart", + "--web-launch-url='$url'", + "--device-id=$deviceId", + ) + } +} diff --git a/learning/tour-of-beam/frontend/integration_test/app_test.dart b/learning/tour-of-beam/frontend/integration_test/common/common.dart similarity index 54% rename from learning/tour-of-beam/frontend/integration_test/app_test.dart rename to learning/tour-of-beam/frontend/integration_test/common/common.dart index e74b4d35624f..ca9af90470a7 100644 --- a/learning/tour-of-beam/frontend/integration_test/app_test.dart +++ b/learning/tour-of-beam/frontend/integration_test/common/common.dart @@ -16,28 +16,17 @@ * limitations under the License. */ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:playground_components/playground_components.dart'; import 'package:tour_of_beam/main.dart' as app; +import 'package:tour_of_beam/pages/tour/controllers/content_tree.dart'; +import 'package:tour_of_beam/pages/tour/widgets/content_tree.dart'; -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); +Future init(WidgetTester wt) async { + await app.main(); + await wt.pumpAndSettle(); +} - group('theme', () { - testWidgets('mode toggle', (tester) async { - app.main(); - await tester.pumpAndSettle(); - final Finder switchToDarkModeButton = - find.widgetWithText(ToggleThemeButton, 'ui.darkMode'.tr()); - expect(switchToDarkModeButton, findsOneWidget); - await tester.tap(switchToDarkModeButton); - await tester.pumpAndSettle(); - expect( - find.widgetWithText(ToggleThemeButton, 'ui.lightMode'.tr()), - findsOneWidget, - ); - }); - }); +ContentTreeController getContentTreeController(WidgetTester wt) { + return (wt.widget(find.byType(ContentTreeWidget)) as ContentTreeWidget) + .controller; } diff --git a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_separator.dart b/learning/tour-of-beam/frontend/integration_test/common/common_finders.dart similarity index 64% rename from playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_separator.dart rename to learning/tour-of-beam/frontend/integration_test/common/common_finders.dart index 78f8b0d7c34c..7fa728d715c0 100644 --- a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_separator.dart +++ b/learning/tour-of-beam/frontend/integration_test/common/common_finders.dart @@ -16,20 +16,20 @@ * limitations under the License. */ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:playground/constants/sizes.dart'; -import 'package:playground_components/playground_components.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tour_of_beam/components/sdk_dropdown.dart'; -class PipelineOptionsDropdownSeparator extends StatelessWidget { - const PipelineOptionsDropdownSeparator({Key? key}) : super(key: key); +extension CommonFindersExtension on CommonFinders { + Finder sdkDropdown() { + return byType(SdkDropdown); + } - @override - Widget build(BuildContext context) { - return Container( - height: kDividerHeight, - decoration: BoxDecoration( - color: Theme.of(context).extension()?.borderColor, - ), + Finder startTourButton() { + return find.ancestor( + of: find.text('pages.welcome.startTour'.tr()), + matching: find.byType(ElevatedButton), ); } } diff --git a/learning/tour-of-beam/frontend/integration_test/tour_page_test.dart b/learning/tour-of-beam/frontend/integration_test/tour_page_test.dart new file mode 100644 index 000000000000..7d95a66f12b8 --- /dev/null +++ b/learning/tour-of-beam/frontend/integration_test/tour_page_test.dart @@ -0,0 +1,259 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:playground_components/playground_components.dart'; +import 'package:playground_components_dev/playground_components_dev.dart'; +import 'package:tour_of_beam/cache/content_tree.dart'; +import 'package:tour_of_beam/components/builders/content_tree.dart'; +import 'package:tour_of_beam/models/group.dart'; +import 'package:tour_of_beam/models/module.dart'; +import 'package:tour_of_beam/models/unit.dart'; +import 'package:tour_of_beam/pages/tour/screen.dart'; +import 'package:tour_of_beam/pages/tour/state.dart'; +import 'package:tour_of_beam/pages/tour/widgets/playground.dart'; +import 'package:tour_of_beam/pages/tour/widgets/unit.dart'; +import 'package:tour_of_beam/pages/tour/widgets/unit_content.dart'; + +import 'common/common.dart'; +import 'common/common_finders.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + testWidgets( + 'ToB miscellaneous ui', + (wt) async { + await init(wt); + await wt.tapAndSettle(find.text(Sdk.java.title)); + await wt.tapAndSettle(find.startTourButton()); + + await _checkContentTreeBuildsProperly(wt); + await _checkContentTreeScrollsProperly(wt); + await _checkHighlightsSelectedUnit(wt); + await _checkRunCodeWorks(wt); + await _checkResizeUnitContent(wt); + }, + ); +} + +Future _checkContentTreeBuildsProperly(WidgetTester wt) async { + final modules = _getModules(wt); + + for (final module in modules) { + await _checkModule(module, wt); + } +} + +List _getModules(WidgetTester wt) { + final contentTreeCache = GetIt.instance.get(); + final controller = getContentTreeController(wt); + final contentTree = contentTreeCache.getContentTree(controller.sdk); + return contentTree?.modules ?? (throw Exception('Can not load moduled')); +} + +Future _checkModule(ModuleModel module, WidgetTester wt) async { + if (!_getExpandedIds(wt).contains(module.id)) { + await wt.ensureVisible(find.byKey(Key(module.id))); + await wt.tapAndSettle(find.byKey(Key(module.id))); + } + + for (final node in module.nodes) { + if (node is UnitModel) { + await _checkNode(node, wt); + } + if (node is GroupModel) { + await _checkGroup(node, wt); + } + } +} + +Future _checkNode(UnitModel node, WidgetTester wt) async { + final finder = find.byKey(Key(node.id)); + expect(finder, findsOneWidget, reason: node.id); + + await wt.ensureVisible(finder); + expect( + find.descendant( + of: find.byType(ContentTreeBuilder), + matching: find.text(node.title), + ), + findsAtLeastNWidgets(1), + ); + + await _checkUnitContentLoadsProperly(node, wt); +} + +Future _checkUnitContentLoadsProperly( + UnitModel unit, + WidgetTester wt, +) async { + await wt.tapAndSettle(find.byKey(Key(unit.id))); + + final hasSnippet = _getTourNotifier(wt).isUnitContainsSnippet; + + expect( + find.byType(PlaygroundWidget), + hasSnippet ? findsOneWidget : findsNothing, + ); + + expect( + find.descendant( + of: find.byType(UnitContentWidget), + matching: find.text(unit.title), + ), + findsAtLeastNWidgets(1), + ); +} + +Future _checkGroup(GroupModel group, WidgetTester wt) async { + await wt.ensureVisible(find.byKey(Key(group.id))); + await wt.tapAndSettle(find.byKey(Key(group.id))); + + for (final n in group.nodes) { + if (n is GroupModel) { + await _checkGroup(n, wt); + } + if (n is UnitModel) { + await _checkNode(n, wt); + } + } +} + +Future _checkContentTreeScrollsProperly(WidgetTester wt) async { + final modules = _getModules(wt); + final lastNode = modules.expand((m) => m.nodes).whereType().last; + + await wt.ensureVisible(find.byKey(Key(lastNode.id))); + await wt.pumpAndSettle(); +} + +Future _checkHighlightsSelectedUnit(WidgetTester wt) async { + final controller = getContentTreeController(wt); + final selectedUnit = controller.currentNode; + + if (selectedUnit == null) { + fail('No unit selected'); + } + + final selectedUnitText = find.descendant( + of: find.byKey(Key(selectedUnit.id)), + matching: find.text(selectedUnit.title), + skipOffstage: false, + ); + + final selectedUnitContainer = find.ancestor( + of: selectedUnitText, + matching: find.byKey(UnitWidget.containerKey), + ); + + final context = wt.element(selectedUnitText); + + expect( + (wt.widget(selectedUnitContainer).decoration as BoxDecoration?) + ?.color, + Theme.of(context).selectedRowColor, + ); +} + +Future _checkRunCodeWorks(WidgetTester wt) async { + const text = 'OK'; + const code = ''' +public class MyClass { + public static void main(String[] args) { + System.out.print("$text"); + } +} +'''; + + await _selectExampleWithSnippet(wt); + await wt.pumpAndSettle(); + + await wt.enterText(find.snippetCodeField(), code); + await wt.pumpAndSettle(); + + await _runAndCancelExample(wt, const Duration(milliseconds: 300)); + + await wt.tapAndSettle(find.runOrCancelButton()); + + final playgroundController = _getPlaygroundController(wt); + expect( + playgroundController.codeRunner.resultLogOutput, + contains(text), + ); +} + +Future _runAndCancelExample(WidgetTester wt, Duration duration) async { + await wt.tap(find.runOrCancelButton()); + + await wt.pumpAndSettleNoException(timeout: duration); + await wt.tapAndSettle(find.runOrCancelButton()); + + final playgroundController = _getPlaygroundController(wt); + expect( + playgroundController.codeRunner.resultLogOutput, + contains('Pipeline cancelled'), + ); +} + +Future _checkResizeUnitContent(WidgetTester wt) async { + var dragHandleFinder = find.byKey(TourScreen.dragHandleKey); + + final startHandlePosition = wt.getCenter(dragHandleFinder); + + await wt.drag(dragHandleFinder, const Offset(100, 0)); + await wt.pumpAndSettle(); + + dragHandleFinder = find.byKey(TourScreen.dragHandleKey); + + final movedHandlePosition = wt.getCenter(dragHandleFinder); + + expectSimilar(startHandlePosition.dx, movedHandlePosition.dx - 100); +} + +Future _selectExampleWithSnippet(WidgetTester wt) async { + final tourNotifier = _getTourNotifier(wt); + final modules = _getModules(wt); + + for (final module in modules) { + for (final node in module.nodes) { + if (node is UnitModel) { + await _checkNode(node, wt); + if (tourNotifier.isUnitContainsSnippet) { + return; + } + } + } + } +} + +TourNotifier _getTourNotifier(WidgetTester wt) { + return (wt.widget(find.byType(UnitContentWidget)) as UnitContentWidget) + .tourNotifier; +} + +PlaygroundController _getPlaygroundController(WidgetTester wt) { + return _getTourNotifier(wt).playgroundController; +} + +Set _getExpandedIds(WidgetTester wt) { + final controller = getContentTreeController(wt); + return controller.expandedIds; +} diff --git a/learning/tour-of-beam/frontend/integration_test/welcome_page_test.dart b/learning/tour-of-beam/frontend/integration_test/welcome_page_test.dart new file mode 100644 index 000000000000..a4b5b7bfdc9a --- /dev/null +++ b/learning/tour-of-beam/frontend/integration_test/welcome_page_test.dart @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:playground_components_dev/playground_components_dev.dart'; +import 'package:tour_of_beam/cache/content_tree.dart'; +import 'package:tour_of_beam/cache/sdk.dart'; +import 'package:tour_of_beam/main.dart' as app; +import 'package:tour_of_beam/state.dart'; + +import 'common/common_finders.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + testWidgets('mode toggle', (wt) async { + await app.main(); + await wt.pumpAndSettle(); + await checkToggleBrightnessMode(wt); + await _checkSdksLoadedCorrectly(wt); + await _checkSwitchingSdkWorksCorrectly(wt); + }); +} + +Future _checkSdksLoadedCorrectly(WidgetTester wt) async { + // The source of truth of loaded sdks. + final sdkCache = GetIt.instance.get(); + + final sdks = sdkCache.getSdks(); + for (final sdk in sdks) { + expect( + find.outlinedButtonWithText(sdk.title), + findsOneWidget, + ); + } + + // Until we select an SDK the dropdown is not shown. + expect( + find.sdkDropdown(), + findsNothing, + ); + + var button = wt.widget(find.startTourButton()); + expect(button.onPressed, isNull); // Verify it is disabled. + + await wt.tapAndSettle(find.outlinedButtonWithText(sdks[0].title)); + + button = wt.widget(find.startTourButton()); + expect(button.onPressed, isNotNull); // Verify it is enabled. + + await wt.tapAndSettle(find.sdkDropdown()); + + for (final sdk in sdks) { + expect( + find.dropdownMenuItemWithText(sdk.title), + // The current Flutter implementation builds 2 for some reason. + findsAtLeastNWidgets(1), + ); + } + + await wt.sendKeyEvent(LogicalKeyboardKey.escape); + await wt.pumpAndSettle(); +} + +Future _checkSwitchingSdkWorksCorrectly(WidgetTester wt) async { + _checkModulesDisplayed(); + + final sdkCache = GetIt.instance.get(); + + final secondSdk = sdkCache.getSdks()[1]; + await wt.tapAndSettle(find.outlinedButtonWithText(secondSdk.title)); + + _checkModulesDisplayed(); + + await wt.tapAndSettle(find.sdkDropdown()); + final thirdSdk = sdkCache.getSdks()[2]; + + await wt.tapAndSettle( + find.dropdownMenuItemWithText(thirdSdk.title).first, + ); + + _checkModulesDisplayed(); +} + +void _checkModulesDisplayed() { + final contentTreeCache = GetIt.instance.get(); + final appNotifier = GetIt.instance.get(); + final sdkId = appNotifier.sdk; + + if (sdkId == null) { + throw Exception('sdkId is null'); + } + + final contentTree = contentTreeCache.getContentTree(sdkId); + if (contentTree == null) { + throw Exception('contentTree is null'); + } + + for (final module in contentTree.modules) { + expect(find.text(module.title), findsOneWidget); + } +} diff --git a/learning/tour-of-beam/frontend/lib/cache/unit_content.dart b/learning/tour-of-beam/frontend/lib/cache/unit_content.dart index 8470ca3115d8..e474f499b54f 100644 --- a/learning/tour-of-beam/frontend/lib/cache/unit_content.dart +++ b/learning/tour-of-beam/frontend/lib/cache/unit_content.dart @@ -34,7 +34,7 @@ class UnitContentCache extends Cache { String unitId, ) async { final future = _futures[sdkId]?[unitId]; - if (future == null) { + if (future == null || _unitContents[sdkId]![unitId] == null) { await _loadUnitContent(sdkId, unitId); } diff --git a/learning/tour-of-beam/frontend/lib/components/scaffold.dart b/learning/tour-of-beam/frontend/lib/components/scaffold.dart index ea91b4cbe079..bb67351fbff6 100644 --- a/learning/tour-of-beam/frontend/lib/components/scaffold.dart +++ b/learning/tour-of-beam/frontend/lib/components/scaffold.dart @@ -21,6 +21,7 @@ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:playground_components/playground_components.dart'; +import '../pages/tour/widgets/pipeline_options.dart'; import '../state.dart'; import 'footer.dart'; import 'login/button.dart'; @@ -33,7 +34,6 @@ class TobScaffold extends StatelessWidget { final PlaygroundController? playgroundController; const TobScaffold({ - super.key, required this.child, this.playgroundController, }); @@ -44,13 +44,18 @@ class TobScaffold extends StatelessWidget { appBar: AppBar( automaticallyImplyLeading: false, title: const Logo(), - actions: const [ - _ActionVerticalPadding(child: _SdkSelector()), - SizedBox(width: BeamSizes.size12), - _ActionVerticalPadding(child: ToggleThemeButton()), - SizedBox(width: BeamSizes.size6), - _ActionVerticalPadding(child: _Profile()), - SizedBox(width: BeamSizes.size16), + actions: [ + if (playgroundController != null) + _PlaygroundControllerActions( + playgroundController: playgroundController!, + ), + const SizedBox(width: BeamSizes.size12), + const _ActionVerticalPadding(child: _SdkSelector()), + const SizedBox(width: BeamSizes.size12), + const _ActionVerticalPadding(child: ToggleThemeButton()), + const SizedBox(width: BeamSizes.size6), + const _ActionVerticalPadding(child: _Profile()), + const SizedBox(width: BeamSizes.size16), ], ), body: Column( @@ -116,3 +121,38 @@ class _SdkSelector extends StatelessWidget { ); } } + +class _PlaygroundControllerActions extends StatelessWidget { + final PlaygroundController playgroundController; + + const _PlaygroundControllerActions({ + required this.playgroundController, + }); + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: playgroundController, + builder: (context, child) { + final widgets = []; + widgets.add( + _ActionVerticalPadding( + child: TobPipelineOptionsDropdown( + playgroundController: playgroundController, + ), + ), + ); + return Row( + children: widgets + .map( + (e) => Padding( + padding: const EdgeInsets.only(left: BeamSizes.size12), + child: e, + ), + ) + .toList(growable: false), + ); + }, + ); + } +} diff --git a/learning/tour-of-beam/frontend/lib/main.dart b/learning/tour-of-beam/frontend/lib/main.dart index c8493293960c..7d8555acb005 100644 --- a/learning/tour-of-beam/frontend/lib/main.dart +++ b/learning/tour-of-beam/frontend/lib/main.dart @@ -31,7 +31,7 @@ import 'firebase_options.dart'; import 'locator.dart'; import 'router/route_information_parser.dart'; -void main() async { +Future main() async { await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart b/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart index 21bb11017727..761713ef7e76 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart @@ -32,19 +32,28 @@ import 'widgets/unit_content.dart'; class TourScreen extends StatelessWidget { final TourNotifier tourNotifier; + static const dragHandleKey = Key('dragHandleKey'); const TourScreen(this.tourNotifier); @override Widget build(BuildContext context) { - return TobShortcutsManager( - tourNotifier: tourNotifier, - child: TobScaffold( - playgroundController: tourNotifier.playgroundController, - child: MediaQuery.of(context).size.width > ScreenBreakpoints.twoColumns - ? _WideTour(tourNotifier) - : _NarrowTour(tourNotifier), - ), + return AnimatedBuilder( + animation: tourNotifier, + builder: (context, child) { + return TobShortcutsManager( + tourNotifier: tourNotifier, + child: TobScaffold( + playgroundController: tourNotifier.isUnitContainsSnippet + ? tourNotifier.playgroundController + : null, + child: + MediaQuery.of(context).size.width > ScreenBreakpoints.twoColumns + ? _WideTour(tourNotifier) + : _NarrowTour(tourNotifier), + ), + ); + }, ); } } @@ -61,19 +70,40 @@ class _WideTour extends StatelessWidget { children: [ ContentTreeWidget(controller: tourNotifier.contentTreeController), Expanded( - child: SplitView( - direction: Axis.horizontal, - first: UnitContentWidget(tourNotifier), - second: PlaygroundWidget( - tourNotifier: tourNotifier, - ), - ), + child: _UnitContentWidget(tourNotifier: tourNotifier), ), ], ); } } +class _UnitContentWidget extends StatelessWidget { + const _UnitContentWidget({required this.tourNotifier}); + + final TourNotifier tourNotifier; + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: tourNotifier, + builder: (context, widget) { + return !tourNotifier.isUnitContainsSnippet + ? UnitContentWidget(tourNotifier) + : SplitView( + direction: Axis.horizontal, + dragHandleKey: TourScreen.dragHandleKey, + first: UnitContentWidget(tourNotifier), + second: tourNotifier.isSnippetLoading + ? const Center(child: CircularProgressIndicator()) + : PlaygroundWidget( + tourNotifier: tourNotifier, + ), + ); + }, + ); + } +} + class _NarrowTour extends StatelessWidget { final TourNotifier tourNotifier; diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/state.dart b/learning/tour-of-beam/frontend/lib/pages/tour/state.dart index e16e73772ed4..5e7737308f90 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/state.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/state.dart @@ -101,12 +101,16 @@ class TourNotifier extends ChangeNotifier with PageStateMixin { SnippetType _snippetType = SnippetType.original; SnippetType get snippetType => _snippetType; + bool _isLoadingSnippet = false; + SaveCodeStatus _saveCodeStatus = SaveCodeStatus.saved; SaveCodeStatus get saveCodeStatus => _saveCodeStatus; set saveCodeStatus(SaveCodeStatus saveCodeStatus) { _saveCodeStatus = saveCodeStatus; notifyListeners(); } + bool get isUnitContainsSnippet => currentUnitContent?.taskSnippetId != null; + bool get isSnippetLoading => _isLoadingSnippet; Future _onAuthChanged() async { await _unitProgressCache.loadUnitProgress(currentSdk); @@ -133,10 +137,14 @@ class TourNotifier extends ChangeNotifier with PageStateMixin { if (currentNode is! UnitModel) { await _emptyPlayground(); } else { + _setUnitContent(null); + notifyListeners(); + final content = await _unitContentCache.getUnitContent( currentSdk.id, currentNode.id, ); + _setUnitContent(content); await _unitProgressCache.loadUnitProgress(currentSdk); _trySetSnippetType(SnippetType.saved); @@ -146,7 +154,7 @@ class TourNotifier extends ChangeNotifier with PageStateMixin { } void _setUnitContent(UnitContentModel? unitContent) { - if (unitContent == null || unitContent == _currentUnitContent) { + if (unitContent == _currentUnitContent) { return; } @@ -156,7 +164,9 @@ class TourNotifier extends ChangeNotifier with PageStateMixin { _currentUnitContent = unitContent; - _trackUnitOpened(unitContent.id); + if (_currentUnitContent != null) { + _trackUnitOpened(_currentUnitContent!.id); + } } void _trackUnitClosed() { @@ -285,13 +295,22 @@ class TourNotifier extends ChangeNotifier with PageStateMixin { ); break; } - await playgroundController.examplesLoader.load( - ExamplesLoadingDescriptor( - descriptors: [ - descriptor, - ], - ), - ); + + try { + _isLoadingSnippet = true; + notifyListeners(); + + await playgroundController.examplesLoader.load( + ExamplesLoadingDescriptor( + descriptors: [ + descriptor, + ], + ), + ); + } finally { + _isLoadingSnippet = false; + notifyListeners(); + } _fillFeedbackController(); } diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/content_tree.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/content_tree.dart index 85f82eefbe7f..bb4bc1d1fa09 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/content_tree.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/content_tree.dart @@ -44,7 +44,7 @@ class ContentTreeWidget extends StatelessWidget { sdk: controller.sdk, builder: (context, contentTree, child) { if (contentTree == null) { - return Container(); + return const Center(child: CircularProgressIndicator()); } return SingleChildScrollView( diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/group.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/group.dart index c4038a0407ce..685a33eb98c0 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/group.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/group.dart @@ -30,6 +30,7 @@ class GroupWidget extends StatelessWidget { const GroupWidget({ required this.group, required this.contentTreeController, + super.key, }); @override diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/module.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/module.dart index e2d90a9e7f96..b3760baaa39d 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/module.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/module.dart @@ -27,10 +27,10 @@ class ModuleWidget extends StatelessWidget { final ModuleModel module; final ContentTreeController contentTreeController; - const ModuleWidget({ + ModuleWidget({ required this.module, required this.contentTreeController, - }); + }) : super(key: Key(module.id)); @override Widget build(BuildContext context) { diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/node.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/node.dart index c771eff17c90..da8f8adc16ea 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/node.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/node.dart @@ -42,6 +42,7 @@ class NodeWidget extends StatelessWidget { return GroupWidget( group: node, contentTreeController: contentTreeController, + key: Key(node.id), ); } @@ -49,6 +50,7 @@ class NodeWidget extends StatelessWidget { return UnitWidget( unit: node, contentTreeController: contentTreeController, + key: Key(node.id), ); } diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/pipeline_options.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/pipeline_options.dart new file mode 100644 index 000000000000..538d08fd0cd9 --- /dev/null +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/pipeline_options.dart @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/widgets.dart'; +import 'package:playground_components/playground_components.dart'; + +class TobPipelineOptionsDropdown extends StatelessWidget { + final PlaygroundController playgroundController; + + const TobPipelineOptionsDropdown({required this.playgroundController}); + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: playgroundController, + builder: (_, __) { + final pipelineOptions = + playgroundController.snippetEditingController?.pipelineOptions; + return PipelineOptionsDropdown( + pipelineOptions: pipelineOptions ?? '', + setPipelineOptions: playgroundController.setPipelineOptions, + ); + }, + ); + } +} diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/playground.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/playground.dart index 4fe00e085d43..67616650fdd6 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/playground.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/playground.dart @@ -16,6 +16,7 @@ * limitations under the License. */ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:playground_components/playground_components.dart'; @@ -39,6 +40,13 @@ class PlaygroundWidget extends StatelessWidget { Widget _buildOnChange(BuildContext context, Widget? child) { final playgroundController = tourNotifier.playgroundController; + if (playgroundController.codeRunner.result?.errorMessage?.isNotEmpty ?? + false) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _handleError(context, playgroundController); + }); + } + final snippetController = playgroundController.snippetEditingController; if (snippetController == null) { return const LoadingIndicator(); @@ -79,7 +87,7 @@ class PlaygroundWidget extends StatelessWidget { PlaygroundComponents.analyticsService.sendUnawaited( RunStartedAnalyticsEvent( snippetContext: tourNotifier.playgroundController - .codeRunner.eventSnippetContext!, + .codeRunner.eventSnippetContext, trigger: EventTrigger.click, additionalParams: tourNotifier.tobEventContext.toJson(), ), @@ -102,4 +110,16 @@ class PlaygroundWidget extends StatelessWidget { ], ); } + + void _handleError(BuildContext context, PlaygroundController controller) { + //TODO(alexeyinkin): A better trigger instead of resetErrorMessageText, https://github.com/apache/beam/issues/26319 + PlaygroundComponents.toastNotifier.add( + Toast( + description: controller.codeRunner.result?.errorMessage ?? '', + title: 'intents.playground.run'.tr(), + type: ToastType.error, + ), + ); + controller.resetErrorMessageText(); + } } diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/unit.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/unit.dart index c8ee6627a252..d142dcfb743d 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/unit.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/unit.dart @@ -28,10 +28,12 @@ import 'binary_progress.dart'; class UnitWidget extends StatelessWidget { final UnitModel unit; final ContentTreeController contentTreeController; + static const containerKey = Key('UnitContainer'); const UnitWidget({ required this.unit, required this.contentTreeController, + super.key, }); @override @@ -46,6 +48,7 @@ class UnitWidget extends StatelessWidget { return ClickableWidget( onTap: () => contentTreeController.onNodePressed(unit), child: Container( + key: containerKey, decoration: BoxDecoration( color: isSelected ? Theme.of(context).selectedRowColor : null, borderRadius: BorderRadius.circular(BeamSizes.size3), diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/unit_content.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/unit_content.dart index 3b47937a8d43..d02ae333e880 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/unit_content.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/unit_content.dart @@ -86,7 +86,7 @@ class _Content extends StatelessWidget { Widget build(BuildContext context) { final content = unitContent; if (content == null) { - return Container(); + return const Center(child: CircularProgressIndicator()); } return ListView( diff --git a/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart b/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart index 4b6b5eb5e4b1..2d44a5795701 100644 --- a/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart +++ b/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart @@ -129,7 +129,7 @@ class _SdkSelection extends StatelessWidget { SdksBuilder( builder: (context, sdks, child) { if (sdks.isEmpty) { - return Container(); + return const Center(child: CircularProgressIndicator()); } return AnimatedBuilder( @@ -184,7 +184,7 @@ class _TourSummary extends StatelessWidget { sdk: sdk, builder: (context, contentTree, child) { if (contentTree == null) { - return Container(); + return const Center(child: CircularProgressIndicator()); } return Column( diff --git a/learning/tour-of-beam/frontend/lib/router/page_factory.dart b/learning/tour-of-beam/frontend/lib/router/page_factory.dart index 3bfb409aa3c4..b834bc7d7ecb 100644 --- a/learning/tour-of-beam/frontend/lib/router/page_factory.dart +++ b/learning/tour-of-beam/frontend/lib/router/page_factory.dart @@ -29,6 +29,7 @@ class PageFactory { switch (factoryKey) { case TourPage.classFactoryKey: return TourPage.fromStateMap(state); + case WelcomePage.classFactoryKey: return WelcomePage.fromStateMap(state); } diff --git a/learning/tour-of-beam/frontend/pubspec.lock b/learning/tour-of-beam/frontend/pubspec.lock index 8f638918ce4f..08713d667b18 100644 --- a/learning/tour-of-beam/frontend/pubspec.lock +++ b/learning/tour-of-beam/frontend/pubspec.lock @@ -450,10 +450,10 @@ packages: dependency: transitive description: name: flutter_code_editor - sha256: "73313c8235b242102af1935312933134774f62c7ed8ad8297beedb88340cc7e1" + sha256: "88b5d735e838658281dcd2b4b83437d8cdec9152fd174be147233e2f93fb01a9" url: "https://pub.dev" source: hosted - version: "0.2.18" + version: "0.2.20" flutter_driver: dependency: transitive description: flutter @@ -891,6 +891,13 @@ packages: relative: true source: path version: "0.0.1" + playground_components_dev: + dependency: "direct main" + description: + path: "../../../playground/frontend/playground_components_dev" + relative: true + source: path + version: "0.0.1" plugin_platform_interface: dependency: transitive description: diff --git a/learning/tour-of-beam/frontend/pubspec.yaml b/learning/tour-of-beam/frontend/pubspec.yaml index edaec367456c..df297d075c4e 100644 --- a/learning/tour-of-beam/frontend/pubspec.yaml +++ b/learning/tour-of-beam/frontend/pubspec.yaml @@ -48,6 +48,7 @@ dependencies: keyed_collection_widgets: ^0.4.3 markdown: ^7.0.1 playground_components: { path: ../../../playground/frontend/playground_components } + playground_components_dev: { path: ../../../playground/frontend/playground_components_dev } provider: ^6.0.3 rate_limiter: ^1.0.0 shared_preferences: ^2.0.15 diff --git a/playground/frontend/README.md b/playground/frontend/README.md index 7c35b7f88530..7fc4481b83e3 100644 --- a/playground/frontend/README.md +++ b/playground/frontend/README.md @@ -163,9 +163,6 @@ flutter test ### Integration Tests -Integration tests currently can be run only on a local development machine. -Server testing has not been verified yet. - 1. Install Google Chrome: https://www.google.com/chrome/ 2. Install Chrome Driver: https://chromedriver.chromium.org/downloads - Note: This GitHub action installs both Chrome and Chrome Driver: diff --git a/playground/frontend/integration_test/common/common.dart b/playground/frontend/integration_test/common/common.dart index 7ec8318c5a61..1a57bf443b63 100644 --- a/playground/frontend/integration_test/common/common.dart +++ b/playground/frontend/integration_test/common/common.dart @@ -30,10 +30,3 @@ void expectHasDescendant(Finder ancestor, Finder descendant) { findsOneWidget, ); } - -void expectSimilar(double a, double b) { - Matcher closeToFraction(num value, double fraction) => - closeTo(value, value * fraction); - Matcher onePerCentTolerance(num value) => closeToFraction(value, 0.01); - expect(a, onePerCentTolerance(b)); -} diff --git a/playground/frontend/integration_test/common/common_finders.dart b/playground/frontend/integration_test/common/common_finders.dart index 051c51167dd7..c2f1801b25df 100644 --- a/playground/frontend/integration_test/common/common_finders.dart +++ b/playground/frontend/integration_test/common/common_finders.dart @@ -18,8 +18,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown.dart'; -import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_body.dart'; import 'package:playground/modules/editor/components/share_dropdown/link_text_field.dart'; import 'package:playground/modules/editor/components/share_dropdown/share_button.dart'; import 'package:playground/modules/editor/components/share_dropdown/share_tabs_headers.dart'; @@ -32,7 +30,6 @@ import 'package:playground/modules/shortcuts/components/shortcuts_dialog.dart'; import 'package:playground/pages/standalone_playground/widgets/editor_textarea_wrapper.dart'; import 'package:playground/pages/standalone_playground/widgets/more_actions.dart'; import 'package:playground_components/playground_components.dart'; -import 'package:playground_components/src/widgets/drag_handle.dart'; import 'package:playground_components_dev/playground_components_dev.dart'; extension CommonFindersExtension on CommonFinders { diff --git a/playground/frontend/integration_test/embedded_run_test.dart b/playground/frontend/integration_test/embedded_run_test.dart index c4da338b1130..24ad8e78a9ed 100644 --- a/playground/frontend/integration_test/embedded_run_test.dart +++ b/playground/frontend/integration_test/embedded_run_test.dart @@ -21,7 +21,6 @@ import 'package:integration_test/integration_test.dart'; import 'package:playground_components_dev/playground_components_dev.dart'; import 'common/common.dart'; -import 'miscellaneous_ui/toggle_brightness_mode_test.dart'; final _url = '/embedded?path=${javaMinimalWordCount.dbPath}&sdk=java'; diff --git a/playground/frontend/integration_test/standalone_cancel_running_example_test.dart b/playground/frontend/integration_test/standalone_cancel_running_example_test.dart index 9ad565818b8e..e6cb81d3e072 100644 --- a/playground/frontend/integration_test/standalone_cancel_running_example_test.dart +++ b/playground/frontend/integration_test/standalone_cancel_running_example_test.dart @@ -35,7 +35,7 @@ void main() { await _runAndCancelExample(wt, const Duration(milliseconds: 300)); final source = wt.findPlaygroundController().source ?? ''; - await wt.enterText(find.codeField(), '//comment\n' + source); + await wt.enterText(find.snippetCodeField(), '//comment\n' + source); await wt.pumpAndSettle(); // Cancel changed example. diff --git a/playground/frontend/integration_test/standalone_change_example_sdk_run_test.dart b/playground/frontend/integration_test/standalone_change_example_sdk_run_test.dart index 6bf582c7cbe5..00f10d3b8643 100644 --- a/playground/frontend/integration_test/standalone_change_example_sdk_run_test.dart +++ b/playground/frontend/integration_test/standalone_change_example_sdk_run_test.dart @@ -60,7 +60,7 @@ public class MyClass { } '''; - await wt.enterText(find.codeField(), code); + await wt.enterText(find.snippetCodeField(), code); await wt.pumpAndSettle(); await wt.tapAndSettle(find.runOrCancelButton()); @@ -103,7 +103,7 @@ public class MyClass { const text = 'OK'; const code = 'print("$text", end="")'; - await wt.enterText(find.codeField(), code); + await wt.enterText(find.snippetCodeField(), code); await wt.pumpAndSettle(); await wt.tapAndSettle(find.runOrCancelButton()); diff --git a/playground/frontend/integration_test/standalone_change_pipeline_options_and_run_test.dart b/playground/frontend/integration_test/standalone_change_pipeline_options_and_run_test.dart index 80e33c8bb7e6..db6f8ca82359 100644 --- a/playground/frontend/integration_test/standalone_change_pipeline_options_and_run_test.dart +++ b/playground/frontend/integration_test/standalone_change_pipeline_options_and_run_test.dart @@ -19,10 +19,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_body.dart'; -import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_input.dart'; -import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_row.dart'; -import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_text_field.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:playground_components_dev/playground_components_dev.dart'; import 'common/common.dart'; diff --git a/playground/frontend/integration_test/standalone_miscellaneous_ui_test.dart b/playground/frontend/integration_test/standalone_miscellaneous_ui_test.dart index c066ab64b70a..509163bb80ef 100644 --- a/playground/frontend/integration_test/standalone_miscellaneous_ui_test.dart +++ b/playground/frontend/integration_test/standalone_miscellaneous_ui_test.dart @@ -18,6 +18,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:playground_components_dev/playground_components_dev.dart'; import 'common/common.dart'; import 'miscellaneous_ui/description_test.dart'; @@ -28,7 +29,6 @@ import 'miscellaneous_ui/output_placement_test.dart'; import 'miscellaneous_ui/report_issue.dart'; import 'miscellaneous_ui/resize_output_test.dart'; import 'miscellaneous_ui/shortcuts_modal_test.dart'; -import 'miscellaneous_ui/toggle_brightness_mode_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/playground/frontend/lib/constants/sizes.dart b/playground/frontend/lib/constants/sizes.dart index c4515a5e3dc1..743c2a7f466c 100644 --- a/playground/frontend/lib/constants/sizes.dart +++ b/playground/frontend/lib/constants/sizes.dart @@ -25,7 +25,6 @@ const double kXlSpacing = 16.0; const double kXxlSpacing = 36.0; // sizes -const kButtonHeight = 40.0; const kIconButtonSplashRadius = 24.0; // border radius @@ -48,6 +47,3 @@ const double kContainerHeight = 40.0; const double kLabelFontSize = 16.0; const double kTitleFontSize = 18.0; - -//divider size -const double kDividerHeight = 1.0; diff --git a/playground/frontend/lib/l10n/app_en.arb b/playground/frontend/lib/l10n/app_en.arb index 67e58b7b670c..2dd3a8f5d920 100644 --- a/playground/frontend/lib/l10n/app_en.arb +++ b/playground/frontend/lib/l10n/app_en.arb @@ -131,22 +131,6 @@ "@pipelineOptions": { "description": "Title for the Pipeline Options" }, - "rawPipelineOptions": "Raw", - "@rawPipelineOptions": { - "description": "Title for the Raw Pipeline Options Tab" - }, - "optionsPipelineOptions": "Options", - "@optionsPipelineOptions": { - "description": "Title for the Options Pipeline Options Tab" - }, - "saveAndClose": "Save & close", - "@saveAndClose": { - "description": "Text for save and close button on pipeline dropdown" - }, - "addPipelineOptionParameter": "Add parameter", - "@addPipelineOptionParameter": { - "description": "Text for add parameter button on pipeline dropdown" - }, "input": "Input", "@input": { "description": "Text input label" @@ -159,10 +143,6 @@ "@value": { "description": "Text value label" }, - "pipelineOptionsError": "Please check the format (example: --key1 value1 --key2 value2), only alphanumeric and \",*,/,-,:,;,',. symbols are allowed", - "@pipelineOptionsError": { - "description": "Pipeline options parse error" - }, "addExample": "Add your own example", "@addExample": { "description": "Add example link text" diff --git a/playground/frontend/lib/modules/editor/components/share_dropdown/share_button.dart b/playground/frontend/lib/modules/editor/components/share_dropdown/share_button.dart index 29a19dbeb2c9..3a110c49168a 100644 --- a/playground/frontend/lib/modules/editor/components/share_dropdown/share_button.dart +++ b/playground/frontend/lib/modules/editor/components/share_dropdown/share_button.dart @@ -20,7 +20,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:playground_components/playground_components.dart'; -import '../../../../components/dropdown_button/dropdown_button.dart'; import '../../../../services/analytics/events/share_code_clicked.dart'; import 'share_dropdown_body.dart'; diff --git a/playground/frontend/lib/modules/examples/example_selector.dart b/playground/frontend/lib/modules/examples/example_selector.dart index 9950ceaafeba..ad3f872b8bd2 100644 --- a/playground/frontend/lib/modules/examples/example_selector.dart +++ b/playground/frontend/lib/modules/examples/example_selector.dart @@ -24,7 +24,6 @@ import 'package:provider/provider.dart'; import '../../constants/sizes.dart'; import '../../pages/standalone_playground/notifiers/example_selector_state.dart'; -import '../../utils/dropdown_utils.dart'; import 'components/outside_click_handler.dart'; import 'examples_dropdown_content.dart'; import 'models/popover_state.dart'; diff --git a/playground/frontend/lib/modules/sdk/components/sdk_selector.dart b/playground/frontend/lib/modules/sdk/components/sdk_selector.dart index 8bba9ed7e6aa..586ea06074a8 100644 --- a/playground/frontend/lib/modules/sdk/components/sdk_selector.dart +++ b/playground/frontend/lib/modules/sdk/components/sdk_selector.dart @@ -21,7 +21,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:playground_components/playground_components.dart'; import 'package:provider/provider.dart'; -import '../../../components/dropdown_button/dropdown_button.dart'; import '../../../constants/sizes.dart'; import 'sdk_selector_row.dart'; diff --git a/playground/frontend/lib/pages/standalone_playground/screen.dart b/playground/frontend/lib/pages/standalone_playground/screen.dart index 754864a53651..1cd7d5e31370 100644 --- a/playground/frontend/lib/pages/standalone_playground/screen.dart +++ b/playground/frontend/lib/pages/standalone_playground/screen.dart @@ -23,7 +23,6 @@ import '../../components/logo/logo_component.dart'; import '../../constants/sizes.dart'; import '../../modules/actions/components/new_example.dart'; import '../../modules/actions/components/reset.dart'; -import '../../modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown.dart'; import '../../modules/examples/example_selector.dart'; import '../../modules/sdk/components/sdk_selector.dart'; import '../../modules/shortcuts/components/shortcuts_manager.dart'; diff --git a/playground/frontend/lib/pages/standalone_playground/widgets/editor_textarea_wrapper.dart b/playground/frontend/lib/pages/standalone_playground/widgets/editor_textarea_wrapper.dart index 552064498ea1..086a61ed64c7 100644 --- a/playground/frontend/lib/pages/standalone_playground/widgets/editor_textarea_wrapper.dart +++ b/playground/frontend/lib/pages/standalone_playground/widgets/editor_textarea_wrapper.dart @@ -82,6 +82,7 @@ class CodeTextAreaWrapper extends StatelessWidget { } void _handleError(BuildContext context, PlaygroundController controller) { + //TODO(alexeyinkin): A better trigger instead of resetErrorMessageText, https://github.com/apache/beam/issues/26319 PlaygroundComponents.toastNotifier.add( Toast( description: controller.codeRunner.result?.errorMessage ?? '', diff --git a/playground/frontend/playground_components/assets/translations/en.yaml b/playground/frontend/playground_components/assets/translations/en.yaml index 996baed40802..565017e997c9 100644 --- a/playground/frontend/playground_components/assets/translations/en.yaml +++ b/playground/frontend/playground_components/assets/translations/en.yaml @@ -81,6 +81,18 @@ widgets: notificationTitles: run: 'Run Code' cancelExecution: 'Cancel Execution' + + pipelineOptions: + options: 'Options' + raw: 'Raw' + error: > + Please check the format (example: --key1 value1 --key2 value2), only alphanumeric and \",*,/,-,:,;,',. symbols are allowed + addParameter: Add parameter + saveAndClose: Save & close + input: Input + pipelineOptions: Pipeline Options + name: Name + value: Value versions: beam: 'Beam {version}' diff --git a/playground/frontend/playground_components/lib/playground_components.dart b/playground/frontend/playground_components/lib/playground_components.dart index 856a1ba81833..9c7155f8eee4 100644 --- a/playground/frontend/playground_components/lib/playground_components.dart +++ b/playground/frontend/playground_components/lib/playground_components.dart @@ -32,6 +32,7 @@ export 'src/controllers/window_close_notifier/window_close_notifier.dart'; export 'src/enums/complexity.dart'; export 'src/enums/feedback_rating.dart'; export 'src/enums/output_tab.dart'; +export 'src/exceptions/example_loading_exception.dart'; export 'src/models/category_with_examples.dart'; export 'src/models/dataset.dart'; @@ -91,6 +92,7 @@ export 'src/theme/switch_notifier.dart'; export 'src/theme/theme.dart'; export 'src/util/async.dart'; +export 'src/util/dropdown_utils.dart'; export 'src/util/iterable.dart'; export 'src/util/logical_keyboard_key.dart'; export 'src/util/pipeline_options.dart'; @@ -107,6 +109,8 @@ export 'src/widgets/dialog.dart'; export 'src/widgets/dialogs/confirm.dart'; export 'src/widgets/dialogs/progress.dart'; export 'src/widgets/divider.dart'; +export 'src/widgets/drag_handle.dart'; +export 'src/widgets/dropdown_button/dropdown_button.dart'; export 'src/widgets/feedback.dart'; export 'src/widgets/header_icon_button.dart'; export 'src/widgets/loading_error.dart'; @@ -118,6 +122,11 @@ export 'src/widgets/output/result_tab.dart'; export 'src/widgets/overlay/body.dart'; export 'src/widgets/overlay/opener.dart'; export 'src/widgets/overlay/widget.dart'; +export 'src/widgets/pipeline_options_dropdown/pipeline_options_dropdown.dart'; +export 'src/widgets/pipeline_options_dropdown/pipeline_options_dropdown_body.dart'; +export 'src/widgets/pipeline_options_dropdown/pipeline_options_dropdown_input.dart'; +export 'src/widgets/pipeline_options_dropdown/pipeline_options_row.dart'; +export 'src/widgets/pipeline_options_dropdown/pipeline_options_text_field.dart'; export 'src/widgets/reset_button.dart'; export 'src/widgets/run_or_cancel_button.dart'; export 'src/widgets/shortcut_tooltip.dart'; diff --git a/playground/frontend/playground_components/lib/src/constants/sizes.dart b/playground/frontend/playground_components/lib/src/constants/sizes.dart index 8a85dfb61e23..5caad08b3bce 100644 --- a/playground/frontend/playground_components/lib/src/constants/sizes.dart +++ b/playground/frontend/playground_components/lib/src/constants/sizes.dart @@ -39,15 +39,18 @@ class BeamSizes { static const double appBarHeight = 55; static const double buttonHeight = 40; + static const double elevation = 2; static const double headerButtonHeight = 46; static const double loadingIndicator = 40; static const double splitViewSeparator = BeamSizes.size8; static const double tabBarHeight = 50; + static const double textFieldHeight = 50; static const double popupWidth = 420; } class BeamBorderRadius { static const double small = BeamSizes.size4; + static const double medium = BeamSizes.size6; static const double large = BeamSizes.size8; static const double infinite = 1000; // TODO: Use StadiumBorder } @@ -55,7 +58,17 @@ class BeamBorderRadius { class BeamIconSizes { static const double xs = BeamSizes.size8; static const double small = BeamSizes.size16; + static const double medium = BeamSizes.size24; static const double large = BeamSizes.size32; static const double largeSplashRadius = 24; } + +class BeamSpacing { + static const double zero = BeamSizes.size0; + static const double small = BeamSizes.size4; + static const double medium = BeamSizes.size8; + static const double large = BeamSizes.size12; + static const double extraLarge = BeamSizes.size16; + static const double extraExtraLarge = BeamSizes.size36; +} diff --git a/playground/frontend/lib/utils/dropdown_utils.dart b/playground/frontend/playground_components/lib/src/util/dropdown_utils.dart similarity index 95% rename from playground/frontend/lib/utils/dropdown_utils.dart rename to playground/frontend/playground_components/lib/src/util/dropdown_utils.dart index 6ebb251a3df3..df1c1c9f7a6c 100644 --- a/playground/frontend/lib/utils/dropdown_utils.dart +++ b/playground/frontend/playground_components/lib/src/util/dropdown_utils.dart @@ -17,7 +17,8 @@ */ import 'package:flutter/material.dart'; -import 'package:playground/components/dropdown_button/dropdown_button.dart'; + +import '../widgets/dropdown_button/dropdown_button.dart'; const _bottomToDropdown = 10.0; diff --git a/playground/frontend/playground_components/lib/src/widgets/drag_handle.dart b/playground/frontend/playground_components/lib/src/widgets/drag_handle.dart index a663e9c89426..3c8a2f693c9e 100644 --- a/playground/frontend/playground_components/lib/src/widgets/drag_handle.dart +++ b/playground/frontend/playground_components/lib/src/widgets/drag_handle.dart @@ -27,6 +27,7 @@ class DragHandle extends StatelessWidget { const DragHandle({ required this.direction, + super.key, }); @override diff --git a/playground/frontend/lib/components/dropdown_button/dropdown_button.dart b/playground/frontend/playground_components/lib/src/widgets/dropdown_button/dropdown_button.dart similarity index 84% rename from playground/frontend/lib/components/dropdown_button/dropdown_button.dart rename to playground/frontend/playground_components/lib/src/widgets/dropdown_button/dropdown_button.dart index f0e7f991b298..ac51664a780a 100644 --- a/playground/frontend/lib/components/dropdown_button/dropdown_button.dart +++ b/playground/frontend/playground_components/lib/src/widgets/dropdown_button/dropdown_button.dart @@ -17,14 +17,14 @@ */ import 'package:flutter/material.dart'; -import 'package:playground_components/playground_components.dart'; import '../../constants/sizes.dart'; -import '../../utils/dropdown_utils.dart'; +import '../../theme/theme.dart'; +import '../../util/dropdown_utils.dart'; const int kAnimationDurationInMilliseconds = 80; -const Offset kAnimationBeginOffset = Offset(0.0, -0.02); -const Offset kAnimationEndOffset = Offset(0.0, 0.0); +const Offset kAnimationBeginOffset = Offset(0, -0.02); +const Offset kAnimationEndOffset = Offset.zero; /// How to align the button and its dropdown. enum DropdownAlignment { @@ -37,22 +37,24 @@ enum DropdownAlignment { class AppDropdownButton extends StatefulWidget { final Widget buttonText; - final Widget Function(void Function()) createDropdown; + final EdgeInsets buttonPadding; + final Widget Function(void Function() close) createDropdown; + final DropdownAlignment dropdownAlign; final double? height; - final double width; final Widget? leading; final bool showArrow; - final DropdownAlignment dropdownAlign; + final double width; const AppDropdownButton({ super.key, required this.buttonText, required this.createDropdown, required this.width, + this.buttonPadding = const EdgeInsets.all(BeamSpacing.medium), + this.dropdownAlign = DropdownAlignment.left, this.height, this.leading, this.showArrow = true, - this.dropdownAlign = DropdownAlignment.left, }); @override @@ -91,23 +93,23 @@ class _AppDropdownButtonState extends State final ext = Theme.of(context).extension()!; return Container( - height: kContainerHeight, + height: BeamSizes.buttonHeight, decoration: BoxDecoration( color: ext.fieldBackgroundColor, - borderRadius: BorderRadius.circular(kSmBorderRadius), + borderRadius: BorderRadius.circular(BeamBorderRadius.small), ), child: TextButton( key: selectorKey, onPressed: _changeSelectorVisibility, child: Padding( - padding: const EdgeInsets.all(kMdSpacing), + padding: widget.buttonPadding, child: Wrap( alignment: WrapAlignment.center, crossAxisAlignment: WrapCrossAlignment.center, children: [ if (widget.leading != null) Padding( - padding: const EdgeInsets.only(right: kMdSpacing), + padding: const EdgeInsets.only(right: BeamSpacing.medium), child: widget.leading, ), widget.buttonText, @@ -133,9 +135,7 @@ class _AppDropdownButtonState extends State return Stack( children: [ GestureDetector( - onTap: () { - _close(); - }, + onTap: _close, child: Container( color: Colors.transparent, height: double.infinity, @@ -148,14 +148,16 @@ class _AppDropdownButtonState extends State child: SlideTransition( position: offsetAnimation, child: Material( - elevation: kElevation, - borderRadius: BorderRadius.circular(kMdBorderRadius), + elevation: BeamSizes.elevation, + borderRadius: BorderRadius.circular(BeamBorderRadius.medium), child: Container( height: widget.height, width: widget.width, decoration: BoxDecoration( color: Theme.of(context).backgroundColor, - borderRadius: BorderRadius.circular(kMdBorderRadius), + borderRadius: BorderRadius.circular( + BeamBorderRadius.medium, + ), ), child: child, ), diff --git a/playground/frontend/playground_components/lib/src/widgets/output/result_tab_content.dart b/playground/frontend/playground_components/lib/src/widgets/output/result_tab_content.dart index 4da643d6d12a..a47671a5dc8f 100644 --- a/playground/frontend/playground_components/lib/src/widgets/output/result_tab_content.dart +++ b/playground/frontend/playground_components/lib/src/widgets/output/result_tab_content.dart @@ -17,6 +17,7 @@ */ import 'package:flutter/material.dart'; +import 'package:flutter_code_editor/flutter_code_editor.dart'; import '../../constants/sizes.dart'; import '../../controllers/playground_controller.dart'; @@ -38,36 +39,30 @@ class ResultTabContent extends StatefulWidget { class _ResultTabContentState extends State { final ScrollController _scrollController = ScrollController(); + final CodeController _codeController = CodeController(); @override - Widget build(BuildContext context) { - final ext = Theme.of(context).extension()!; + void initState() { + super.initState(); + widget.playgroundController.codeRunner.addListener(_updateText); + widget.playgroundController.resultFilterController.addListener( + _updateText, + ); + _updateText(); + } - return UnreadClearer( - controller: widget.playgroundController.codeRunner.unreadController, - unreadKey: UnreadEntryEnum.result, - child: AnimatedBuilder( - animation: widget.playgroundController.codeRunner, - builder: (context, child) => SingleChildScrollView( - controller: _scrollController, - child: Scrollbar( - thumbVisibility: true, - trackVisibility: true, - controller: _scrollController, - child: Padding( - padding: const EdgeInsets.all(BeamSizes.size16), - child: AnimatedBuilder( - animation: widget.playgroundController.resultFilterController, - builder: (context, child) => SelectableText( - _getText(), - style: ext.codeRootStyle, - ), - ), - ), - ), - ), - ), + void _updateText() { + _codeController.fullText = _getText(); + } + + @override + void dispose() { + _codeController.dispose(); + widget.playgroundController.resultFilterController.removeListener( + _updateText, ); + widget.playgroundController.codeRunner.removeListener(_updateText); + super.dispose(); } String _getText() { @@ -82,4 +77,46 @@ class _ResultTabContentState extends State { return widget.playgroundController.codeRunner.resultLogOutput; } } + + @override + Widget build(BuildContext context) { + final ext = Theme.of(context).extension()!; + + return UnreadClearer( + controller: widget.playgroundController.codeRunner.unreadController, + unreadKey: UnreadEntryEnum.result, + child: ColoredBox( + // TODO(alexeyinkin): Migrate to Material 3: https://github.com/apache/beam/issues/24610 + color: Theme.of(context).backgroundColor, + child: AnimatedBuilder( + animation: widget.playgroundController.codeRunner, + builder: (context, child) => SingleChildScrollView( + controller: _scrollController, + child: Scrollbar( + thumbVisibility: true, + trackVisibility: true, + controller: _scrollController, + child: Padding( + padding: const EdgeInsets.all(BeamSizes.size16), + child: AnimatedBuilder( + animation: widget.playgroundController.resultFilterController, + builder: (context, child) { + return CodeTheme( + data: ext.codeTheme, + child: CodeField( + readOnly: true, + controller: _codeController, + gutterStyle: GutterStyle.none, + textStyle: ext.codeRootStyle, + ), + ); + }, + ), + ), + ), + ), + ), + ), + ); + } } diff --git a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_option_controller.dart b/playground/frontend/playground_components/lib/src/widgets/pipeline_options_dropdown/pipeline_option_controller.dart similarity index 100% rename from playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_option_controller.dart rename to playground/frontend/playground_components/lib/src/widgets/pipeline_options_dropdown/pipeline_option_controller.dart diff --git a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_option_label.dart b/playground/frontend/playground_components/lib/src/widgets/pipeline_options_dropdown/pipeline_option_label.dart similarity index 79% rename from playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_option_label.dart rename to playground/frontend/playground_components/lib/src/widgets/pipeline_options_dropdown/pipeline_option_label.dart index 733c43397275..ad2c8eb32a00 100644 --- a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_option_label.dart +++ b/playground/frontend/playground_components/lib/src/widgets/pipeline_options_dropdown/pipeline_option_label.dart @@ -17,19 +17,17 @@ */ import 'package:flutter/material.dart'; -import 'package:playground/constants/font_weight.dart'; -import 'package:playground/constants/sizes.dart'; class PipelineOptionLabel extends StatelessWidget { final String text; - const PipelineOptionLabel({Key? key, required this.text}) : super(key: key); + const PipelineOptionLabel({super.key, required this.text}); @override Widget build(BuildContext context) { return Text( text, - style: const TextStyle(fontWeight: kMediumWeight, fontSize: kLabelFontSize), + style: Theme.of(context).textTheme.labelLarge, ); } } diff --git a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown.dart b/playground/frontend/playground_components/lib/src/widgets/pipeline_options_dropdown/pipeline_options_dropdown.dart similarity index 78% rename from playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown.dart rename to playground/frontend/playground_components/lib/src/widgets/pipeline_options_dropdown/pipeline_options_dropdown.dart index ebf65a6c3ff0..9f67effbdda5 100644 --- a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown.dart +++ b/playground/frontend/playground_components/lib/src/widgets/pipeline_options_dropdown/pipeline_options_dropdown.dart @@ -16,10 +16,11 @@ * limitations under the License. */ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:playground/components/dropdown_button/dropdown_button.dart'; -import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_body.dart'; + +import '../dropdown_button/dropdown_button.dart'; +import 'pipeline_options_dropdown_body.dart'; const kDropdownWidth = 400.0; const kDropdownHeight = 375.0; @@ -29,16 +30,15 @@ class PipelineOptionsDropdown extends StatelessWidget { final void Function(String) setPipelineOptions; const PipelineOptionsDropdown({ - Key? key, + super.key, required this.pipelineOptions, required this.setPipelineOptions, - }) : super(key: key); + }); @override Widget build(BuildContext context) { - AppLocalizations appLocale = AppLocalizations.of(context)!; return AppDropdownButton( - buttonText: Text(appLocale.pipelineOptions), + buttonText: Text('widgets.pipelineOptions.pipelineOptions'.tr()), height: kDropdownHeight, width: kDropdownWidth, createDropdown: (close) => PipelineOptionsDropdownBody( diff --git a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_body.dart b/playground/frontend/playground_components/lib/src/widgets/pipeline_options_dropdown/pipeline_options_dropdown_body.dart similarity index 80% rename from playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_body.dart rename to playground/frontend/playground_components/lib/src/widgets/pipeline_options_dropdown/pipeline_options_dropdown_body.dart index ca12319525b3..6706205b0ff4 100644 --- a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_body.dart +++ b/playground/frontend/playground_components/lib/src/widgets/pipeline_options_dropdown/pipeline_options_dropdown_body.dart @@ -16,15 +16,12 @@ * limitations under the License. */ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:playground/constants/colors.dart'; -import 'package:playground/constants/sizes.dart'; -import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_option_controller.dart'; -import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_input.dart'; -import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_separator.dart'; -import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_form.dart'; -import 'package:playground_components/playground_components.dart'; + +import '../../../playground_components.dart'; +import 'pipeline_option_controller.dart'; +import 'pipeline_options_form.dart'; const kOptionsTabIndex = 0; const kRawTabIndex = 1; @@ -105,7 +102,6 @@ class _PipelineOptionsDropdownBodyState @override Widget build(BuildContext context) { - AppLocalizations appLocale = AppLocalizations.of(context)!; return Column( children: [ TabBar( @@ -113,18 +109,18 @@ class _PipelineOptionsDropdownBodyState tabs: [ Tab( key: PipelineOptionsDropdownBody.optionsTabKey, - text: appLocale.optionsPipelineOptions, + text: 'widgets.pipelineOptions.options'.tr(), ), Tab( key: PipelineOptionsDropdownBody.rawTabKey, - text: appLocale.rawPipelineOptions, + text: 'widgets.pipelineOptions.raw'.tr(), ), ], ), - const PipelineOptionsDropdownSeparator(), + const BeamDivider(), Expanded( child: Padding( - padding: const EdgeInsets.all(kXlSpacing), + padding: const EdgeInsets.all(BeamSpacing.extraLarge), child: TabBarView( controller: tabController, physics: const NeverScrollableScrollPhysics(), @@ -140,27 +136,26 @@ class _PipelineOptionsDropdownBodyState ), ), ), - const PipelineOptionsDropdownSeparator(), + const BeamDivider(), Padding( - padding: const EdgeInsets.all(kXlSpacing), + padding: const EdgeInsets.all(BeamSpacing.extraLarge), child: Row( - crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox( - height: kButtonHeight, + height: BeamSizes.buttonHeight, child: ElevatedButton( key: PipelineOptionsDropdownBody.saveAndCloseButtonKey, - child: Text(appLocale.saveAndClose), + child: Text('widgets.pipelineOptions.saveAndClose'.tr()), onPressed: () => _save(context), ), ), - const SizedBox(width: kLgSpacing), + const SizedBox(width: BeamSpacing.large), if (selectedTab == kOptionsTabIndex) SizedBox( - height: kButtonHeight, + height: BeamSizes.buttonHeight, child: OutlinedButton( key: PipelineOptionsDropdownBody.addOptionButtonKey, - child: Text(appLocale.addPipelineOptionParameter), + child: Text('widgets.pipelineOptions.addParameter'.tr()), onPressed: () => setState(() { pipelineOptionsList.add(PipelineOptionController()); }), @@ -169,11 +164,11 @@ class _PipelineOptionsDropdownBodyState if (showError && selectedTab == kRawTabIndex) Flexible( child: Text( - appLocale.pipelineOptionsError, + 'widgets.pipelineOptions.error'.tr(), style: Theme.of(context) .textTheme .caption! - .copyWith(color: kErrorNotificationColor), + .copyWith(color: BeamNotificationColors.error), softWrap: true, ), ), @@ -203,7 +198,7 @@ class _PipelineOptionsDropdownBodyState return pipelineOptionsToString(pipelineOptionsListValue); } - _save(BuildContext context) { + void _save(BuildContext context) { if (selectedTab == kRawTabIndex && !_isPipelineOptionsTextValid()) { setState(() { showError = true; @@ -220,14 +215,14 @@ class _PipelineOptionsDropdownBodyState return options.isEmpty || (parsedOptions != null); } - _updateRawValue() { + void _updateRawValue() { if (pipelineOptionsListValue.isNotEmpty) { pipelineOptionsController.text = pipelineOptionsToString(pipelineOptionsListValue); } } - _updateFormValue() { + void _updateFormValue() { final parsedOptions = _pipelineOptionsMapToList(pipelineOptionsController.text); if (parsedOptions.isNotEmpty) { diff --git a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_input.dart b/playground/frontend/playground_components/lib/src/widgets/pipeline_options_dropdown/pipeline_options_dropdown_input.dart similarity index 76% rename from playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_input.dart rename to playground/frontend/playground_components/lib/src/widgets/pipeline_options_dropdown/pipeline_options_dropdown_input.dart index 3174cb2cc496..acb736800906 100644 --- a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_input.dart +++ b/playground/frontend/playground_components/lib/src/widgets/pipeline_options_dropdown/pipeline_options_dropdown_input.dart @@ -16,10 +16,11 @@ * limitations under the License. */ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_option_label.dart'; -import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_text_field.dart'; + +import 'pipeline_option_label.dart'; +import 'pipeline_options_text_field.dart'; const kPipelineOptionsInputLines = 8; @@ -29,17 +30,16 @@ class PipelineOptionsDropdownInput extends StatelessWidget { final TextEditingController controller; const PipelineOptionsDropdownInput({ - Key? key, + super.key, required this.controller, - }) : super(key: key); + }); @override Widget build(BuildContext context) { - AppLocalizations appLocale = AppLocalizations.of(context)!; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - PipelineOptionLabel(text: appLocale.input), + PipelineOptionLabel(text: 'widgets.pipelineOptions.input'.tr()), PipelineOptionsTextField( key: textFieldKey, lines: kPipelineOptionsInputLines, diff --git a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_form.dart b/playground/frontend/playground_components/lib/src/widgets/pipeline_options_dropdown/pipeline_options_form.dart similarity index 65% rename from playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_form.dart rename to playground/frontend/playground_components/lib/src/widgets/pipeline_options_dropdown/pipeline_options_form.dart index 61b8cdef5845..d52a2bcc679c 100644 --- a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_form.dart +++ b/playground/frontend/playground_components/lib/src/widgets/pipeline_options_dropdown/pipeline_options_form.dart @@ -17,36 +17,44 @@ */ import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:playground/constants/sizes.dart'; -import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_option_controller.dart'; -import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_option_label.dart'; -import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_row.dart'; -const kSpace = SizedBox(width: kMdSpacing); +import '../../constants/sizes.dart'; +import 'pipeline_option_controller.dart'; +import 'pipeline_option_label.dart'; +import 'pipeline_options_row.dart'; + +const kSpace = SizedBox(width: BeamSpacing.medium); class PipelineOptionsForm extends StatelessWidget { final List options; final void Function(int) onDelete; const PipelineOptionsForm({ - Key? key, + super.key, required this.options, required this.onDelete, - }) : super(key: key); + }); @override Widget build(BuildContext context) { - AppLocalizations appLocale = AppLocalizations.of(context)!; return Column( children: [ Row( children: [ - Expanded(child: PipelineOptionLabel(text: appLocale.name)), + Expanded( + child: PipelineOptionLabel( + text: 'widgets.pipelineOptions.name'.tr(), + ), + ), kSpace, - Expanded(child: PipelineOptionLabel(text: appLocale.value)), - const SizedBox(width: kIconSizeLg), + Expanded( + child: PipelineOptionLabel( + text: 'widgets.pipelineOptions.value'.tr(), + ), + ), + const SizedBox(width: BeamIconSizes.large), ], ), ...options.mapIndexed( diff --git a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_row.dart b/playground/frontend/playground_components/lib/src/widgets/pipeline_options_dropdown/pipeline_options_row.dart similarity index 69% rename from playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_row.dart rename to playground/frontend/playground_components/lib/src/widgets/pipeline_options_dropdown/pipeline_options_row.dart index 126f9c9170f3..c37336199318 100644 --- a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_row.dart +++ b/playground/frontend/playground_components/lib/src/widgets/pipeline_options_dropdown/pipeline_options_row.dart @@ -17,14 +17,12 @@ */ import 'package:flutter/material.dart'; -import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_option_controller.dart'; -import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_form.dart'; -import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_text_field.dart'; -import '../../../../constants/colors.dart'; -import '../../../../constants/sizes.dart'; - -const kTextFieldHeight = 50.0; +import '../../constants/colors.dart'; +import '../../constants/sizes.dart'; +import 'pipeline_option_controller.dart'; +import 'pipeline_options_form.dart'; +import 'pipeline_options_text_field.dart'; class PipelineOptionsRow extends StatelessWidget { final void Function() onDelete; @@ -41,7 +39,7 @@ class PipelineOptionsRow extends StatelessWidget { children: [ Expanded( child: SizedBox( - height: kTextFieldHeight, + height: BeamSizes.textFieldHeight, child: PipelineOptionsTextField( controller: controller.nameController, ), @@ -50,23 +48,23 @@ class PipelineOptionsRow extends StatelessWidget { kSpace, Expanded( child: SizedBox( - height: kTextFieldHeight, + height: BeamSizes.textFieldHeight, child: PipelineOptionsTextField( controller: controller.valueController, ), ), ), SizedBox( - width: kIconSizeLg, + width: BeamIconSizes.large, child: IconButton( - iconSize: kIconSizeMd, - splashRadius: kIconButtonSplashRadius, - icon: const Icon( + iconSize: BeamIconSizes.medium, + splashRadius: BeamIconSizes.largeSplashRadius, + icon: Icon( Icons.delete_outlined, - color: kLightPrimary, + color: Theme.of(context).primaryColor, ), color: Theme.of(context).dividerColor, - onPressed: () => onDelete(), + onPressed: onDelete, ), ), ], diff --git a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_text_field.dart b/playground/frontend/playground_components/lib/src/widgets/pipeline_options_dropdown/pipeline_options_text_field.dart similarity index 81% rename from playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_text_field.dart rename to playground/frontend/playground_components/lib/src/widgets/pipeline_options_dropdown/pipeline_options_text_field.dart index d4202b125697..66c0eb3c79b5 100644 --- a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_text_field.dart +++ b/playground/frontend/playground_components/lib/src/widgets/pipeline_options_dropdown/pipeline_options_text_field.dart @@ -17,18 +17,19 @@ */ import 'package:flutter/material.dart'; -import 'package:playground/constants/sizes.dart'; -import 'package:playground_components/playground_components.dart'; + +import '../../constants/sizes.dart'; +import '../../theme/theme.dart'; class PipelineOptionsTextField extends StatelessWidget { final TextEditingController controller; final int lines; const PipelineOptionsTextField({ - Key? key, + super.key, required this.controller, this.lines = 1, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -37,20 +38,20 @@ class PipelineOptionsTextField extends StatelessWidget { return Container( margin: const EdgeInsets.only( - top: kMdSpacing, + top: BeamSpacing.medium, ), decoration: BoxDecoration( color: Theme.of(context).backgroundColor, - borderRadius: BorderRadius.circular(kMdBorderRadius), + borderRadius: BorderRadius.circular(BeamBorderRadius.medium), ), child: ClipRRect( - borderRadius: BorderRadius.circular(kMdBorderRadius), + borderRadius: BorderRadius.circular(BeamBorderRadius.medium), child: TextFormField( minLines: lines, maxLines: lines, controller: controller, decoration: InputDecoration( - contentPadding: const EdgeInsets.all(kMdSpacing), + contentPadding: const EdgeInsets.all(BeamSpacing.medium), border: _getInputBorder(ext.borderColor), focusedBorder: _getInputBorder(themeData.primaryColor), ), @@ -62,7 +63,7 @@ class PipelineOptionsTextField extends StatelessWidget { OutlineInputBorder _getInputBorder(Color color) { return OutlineInputBorder( borderSide: BorderSide(color: color), - borderRadius: BorderRadius.circular(kMdBorderRadius), + borderRadius: BorderRadius.circular(BeamBorderRadius.medium), ); } } diff --git a/playground/frontend/playground_components/lib/src/widgets/split_view.dart b/playground/frontend/playground_components/lib/src/widgets/split_view.dart index 6291161f135d..447710153d71 100644 --- a/playground/frontend/playground_components/lib/src/widgets/split_view.dart +++ b/playground/frontend/playground_components/lib/src/widgets/split_view.dart @@ -30,12 +30,14 @@ class SplitView extends StatefulWidget { final Widget second; final Axis direction; final double initialRatio; + final Key? dragHandleKey; const SplitView({ super.key, required this.first, required this.second, required this.direction, + this.dragHandleKey, this.initialRatio = defaultRatio, }); @@ -124,7 +126,10 @@ class _SplitViewState extends State { height: _isVertical ? BeamSizes.splitViewSeparator : double.infinity, color: Theme.of(context).dividerColor, child: Center( - child: DragHandle(direction: widget.direction), + child: DragHandle( + direction: widget.direction, + key: widget.dragHandleKey, + ), ), ), onPanUpdate: (DragUpdateDetails details) { diff --git a/playground/frontend/playground_components/pubspec.yaml b/playground/frontend/playground_components/pubspec.yaml index 52a45c64de1f..4dfe7ff0d2f1 100644 --- a/playground/frontend/playground_components/pubspec.yaml +++ b/playground/frontend/playground_components/pubspec.yaml @@ -36,7 +36,7 @@ dependencies: enum_map: ^0.2.1 equatable: ^2.0.5 flutter: { sdk: flutter } - flutter_code_editor: ^0.2.14 + flutter_code_editor: ^0.2.20 flutter_markdown: ^0.6.12 flutter_svg: ^2.0.1 fluttertoast: ^8.1.1 diff --git a/playground/frontend/playground_components_dev/lib/playground_components_dev.dart b/playground/frontend/playground_components_dev/lib/playground_components_dev.dart index c37f6182c9dc..d02b9448230f 100644 --- a/playground/frontend/playground_components_dev/lib/playground_components_dev.dart +++ b/playground/frontend/playground_components_dev/lib/playground_components_dev.dart @@ -18,6 +18,8 @@ export 'src/common_finders.dart'; +export 'src/common_tests/toggle_brightness_mode_test.dart'; + export 'src/examples/example_descriptor.dart'; export 'src/examples/go/example.dart'; diff --git a/playground/frontend/playground_components_dev/lib/src/common_finders.dart b/playground/frontend/playground_components_dev/lib/src/common_finders.dart index 9fb5380dc408..5b57532ebdfe 100644 --- a/playground/frontend/playground_components_dev/lib/src/common_finders.dart +++ b/playground/frontend/playground_components_dev/lib/src/common_finders.dart @@ -24,8 +24,18 @@ import 'package:playground_components/playground_components.dart'; import 'finder.dart'; extension CommonFindersExtension on CommonFinders { - Finder codeField() { - return byType(CodeField); + Finder snippetCodeField() { + return find.descendant( + of: find.byType(SnippetEditor), + matching: byType(CodeField), + ); + } + + Finder dropdownMenuItemWithText(String text) { + return find.descendant( + of: find.byType(DropdownMenuItem), + matching: find.text(text), + ); } Finder graphTab() { @@ -33,10 +43,17 @@ extension CommonFindersExtension on CommonFinders { return widgetWithText(OutputTab, 'Graph'); } - Finder outputSelectableText() { + Finder outputCodeField() { return find.descendant( of: find.outputWidget(), - matching: find.byType(SelectableText), + matching: byType(CodeField), + ); + } + + Finder outlinedButtonWithText(String text) { + return find.descendant( + of: find.byType(OutlinedButton), + matching: find.text(text), ); } diff --git a/playground/frontend/integration_test/miscellaneous_ui/toggle_brightness_mode_test.dart b/playground/frontend/playground_components_dev/lib/src/common_tests/toggle_brightness_mode_test.dart similarity index 95% rename from playground/frontend/integration_test/miscellaneous_ui/toggle_brightness_mode_test.dart rename to playground/frontend/playground_components_dev/lib/src/common_tests/toggle_brightness_mode_test.dart index 07f039f2d9d2..d094b20197b4 100644 --- a/playground/frontend/integration_test/miscellaneous_ui/toggle_brightness_mode_test.dart +++ b/playground/frontend/playground_components_dev/lib/src/common_tests/toggle_brightness_mode_test.dart @@ -18,8 +18,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; + import 'package:playground_components/playground_components.dart'; -import 'package:playground_components_dev/playground_components_dev.dart'; +import '../expect.dart'; +import '../widget_tester.dart'; Future checkToggleBrightnessMode(WidgetTester wt) async { final oldBrightness = wt.getBrightness(); diff --git a/playground/frontend/playground_components_dev/lib/src/expect.dart b/playground/frontend/playground_components_dev/lib/src/expect.dart index 0a371dc3a443..71fe748d02d4 100644 --- a/playground/frontend/playground_components_dev/lib/src/expect.dart +++ b/playground/frontend/playground_components_dev/lib/src/expect.dart @@ -24,6 +24,21 @@ import 'package:playground_components/playground_components.dart'; import 'examples/example_descriptor.dart'; import 'widget_tester.dart'; +void expectContextLine(int contextLine1Based, WidgetTester wt) { + final controller = wt.findOneCodeController(); + final selection = controller.selection; + final position = controller.code.hiddenRanges.recoverPosition( + selection.baseOffset, + placeHiddenRanges: TextAffinity.downstream, + ); + + expect(selection.isCollapsed, true); + expect( + controller.code.lines.characterIndexToLineIndex(position), + contextLine1Based - 1, + ); +} + void expectOutput(ExampleDescriptor example, WidgetTester wt) { if (example.outputTail != null) { expectOutputEndsWith(example.outputTail, wt); @@ -64,27 +79,19 @@ void expectSdk(Sdk sdk, WidgetTester wt) { expect(controller.sdk, sdk); } +void expectSimilar(double a, double b) { + Matcher closeToFraction(num value, double fraction) => + closeTo(value, value * fraction); + Matcher onePerCentTolerance(num value) => closeToFraction(value, 0.01); + expect(a, onePerCentTolerance(b)); +} + void expectVisibleText(String? visibleText, WidgetTester wt) { final controller = wt.findOneCodeController(); expect(visibleText, isNotNull); expect(controller.text, visibleText); } -void expectContextLine(int contextLine1Based, WidgetTester wt) { - final controller = wt.findOneCodeController(); - final selection = controller.selection; - final position = controller.code.hiddenRanges.recoverPosition( - selection.baseOffset, - placeHiddenRanges: TextAffinity.downstream, - ); - - expect(selection.isCollapsed, true); - expect( - controller.code.lines.characterIndexToLineIndex(position), - contextLine1Based - 1, - ); -} - void expectLastAnalyticsEvent( AnalyticsEvent event, { String? reason, diff --git a/playground/frontend/playground_components_dev/lib/src/widget_tester.dart b/playground/frontend/playground_components_dev/lib/src/widget_tester.dart index 342da4326e5d..83c7f416ba8c 100644 --- a/playground/frontend/playground_components_dev/lib/src/widget_tester.dart +++ b/playground/frontend/playground_components_dev/lib/src/widget_tester.dart @@ -32,7 +32,7 @@ import 'expect.dart'; extension WidgetTesterExtension on WidgetTester { //workaround for https://github.com/flutter/flutter/issues/120060 Future enterCodeFieldText(String text) async { - final codeField = widget(find.codeField()); + final codeField = widget(find.snippetCodeField()); (codeField as CodeField).controller.fullText = text; codeField.focusNode?.requestFocus(); } @@ -48,7 +48,7 @@ extension WidgetTesterExtension on WidgetTester { } CodeController findOneCodeController() { - final codeField = find.codeField(); + final codeField = find.snippetCodeField(); expect(codeField, findsOneWidget); return widget(codeField).controller; @@ -66,14 +66,12 @@ extension WidgetTesterExtension on WidgetTester { } String? findOutputText() { - final selectableText = find.outputSelectableText(); - expect(selectableText, findsOneWidget); - - return widget(selectableText).data; + final codeField = widget(find.outputCodeField()); + return (codeField as CodeField).controller.text; } PlaygroundController findPlaygroundController() { - final context = element(find.codeField()); + final context = element(find.snippetCodeField()); return context.read(); } diff --git a/playground/frontend/playground_components_dev/pubspec.yaml b/playground/frontend/playground_components_dev/pubspec.yaml index 5ad79317ebf5..1b1b81fcea28 100644 --- a/playground/frontend/playground_components_dev/pubspec.yaml +++ b/playground/frontend/playground_components_dev/pubspec.yaml @@ -27,7 +27,7 @@ environment: dependencies: app_state: ^0.9.3 flutter: { sdk: flutter } - flutter_code_editor: ^0.2.13 + flutter_code_editor: ^0.2.20 flutter_test: { sdk: flutter } get_it: ^7.2.0 highlight: ^0.7.0 diff --git a/playground/frontend/pubspec.lock b/playground/frontend/pubspec.lock index 2891b4dc3309..d676e72c7c5d 100644 --- a/playground/frontend/pubspec.lock +++ b/playground/frontend/pubspec.lock @@ -394,10 +394,10 @@ packages: dependency: "direct dev" description: name: flutter_code_editor - sha256: "85d0d159ce4f33f3aabedea92c0bdc1ad05c6931bba80ac46660f90c7ab6255c" + sha256: "88b5d735e838658281dcd2b4b83437d8cdec9152fd174be147233e2f93fb01a9" url: "https://pub.dev" source: hosted - version: "0.2.14" + version: "0.2.20" flutter_driver: dependency: transitive description: flutter diff --git a/playground/frontend/pubspec.yaml b/playground/frontend/pubspec.yaml index b92195a74e6d..ea08d33a3faf 100644 --- a/playground/frontend/pubspec.yaml +++ b/playground/frontend/pubspec.yaml @@ -52,7 +52,7 @@ dependencies: dev_dependencies: build_runner: ^2.1.4 fake_async: ^1.3.0 - flutter_code_editor: ^0.2.13 + flutter_code_editor: ^0.2.20 flutter_lints: ^2.0.1 flutter_test: { sdk: flutter } integration_test: { sdk: flutter } diff --git a/settings.gradle.kts b/settings.gradle.kts index a334f56926b3..dd59f4498433 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -59,6 +59,9 @@ include(":examples:java:cdap:servicenow") include(":examples:java:cdap:zendesk") include(":examples:kotlin") include(":examples:multi-language") +include(":learning") +include(":learning:tour-of-beam") +include(":learning:tour-of-beam:frontend") include(":model:fn-execution") include(":model:job-management") include(":model:pipeline")