diff --git a/.github/workflows/ios_ci.yaml b/.github/workflows/ios_ci.yaml index a39a2704c817..ef64a9741da8 100644 --- a/.github/workflows/ios_ci.yaml +++ b/.github/workflows/ios_ci.yaml @@ -97,21 +97,25 @@ jobs: cargo make --profile development-ios-arm64-sim appflowy-core-dev-ios cargo make --profile development-ios-arm64-sim code_generation - # - uses: futureware-tech/simulator-action@v3 - # id: simulator-action - # with: - # model: "iPhone 15" - # shutdown_after_job: false - - # - name: Run AppFlowy on simulator - # working-directory: frontend/appflowy_flutter - # run: | - # flutter run -d ${{ steps.simulator-action.outputs.udid }} & - # pid=$! - # sleep 500 - # kill $pid - # continue-on-error: true - - # - name: Run integration tests - # working-directory: frontend/appflowy_flutter - # run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }} + - uses: futureware-tech/simulator-action@v3 + id: simulator-action + with: + model: "iPhone 15" + shutdown_after_job: false + + - name: Run AppFlowy on simulator + working-directory: frontend/appflowy_flutter + run: | + flutter run -d ${{ steps.simulator-action.outputs.udid }} & + pid=$! + sleep 500 + kill $pid + continue-on-error: true + + - name: Run integration tests + working-directory: frontend/appflowy_flutter + # The integration tests are flaky and sometimes fail with "Connection timed out": + # Don't block the CI. If the tests fail, the CI will still pass. + # Instead, we're using Code Magic to re-run the tests to check if they pass. + continue-on-error: true + run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }} diff --git a/.github/workflows/mobile_ci.yml b/.github/workflows/mobile_ci.yml new file mode 100644 index 000000000000..fbc17b7b70cd --- /dev/null +++ b/.github/workflows/mobile_ci.yml @@ -0,0 +1,82 @@ +name: Mobile-CI + +on: + workflow_dispatch: + inputs: + branch: + description: "Branch to build" + required: true + default: "main" + workflow_id: + description: "Codemagic workflow ID" + required: true + default: "ios-workflow" + type: choice + options: + - ios-workflow + - android-workflow + +env: + CODEMAGIC_API_TOKEN: ${{ secrets.CODEMAGIC_API_TOKEN }} + APP_ID: "6731d2f427e7c816080c3674" + +jobs: + trigger-mobile-build: + runs-on: ubuntu-latest + steps: + - name: Trigger Codemagic Build + id: trigger_build + run: | + RESPONSE=$(curl -X POST \ + --header "Content-Type: application/json" \ + --header "x-auth-token: $CODEMAGIC_API_TOKEN" \ + --data '{ + "appId": "${{ env.APP_ID }}", + "workflowId": "${{ github.event.inputs.workflow_id }}", + "branch": "${{ github.event.inputs.branch }}" + }' \ + https://api.codemagic.io/builds) + + BUILD_ID=$(echo $RESPONSE | jq -r '.buildId') + echo "build_id=$BUILD_ID" >> $GITHUB_OUTPUT + echo "build_id=$BUILD_ID" + + - name: Wait for build and check status + id: check_status + run: | + while true; do + RESPONSE=$(curl -X GET \ + --header "Content-Type: application/json" \ + --header "x-auth-token: $CODEMAGIC_API_TOKEN" \ + https://api.codemagic.io/builds/${{ steps.trigger_build.outputs.build_id }}) + + STATUS=$(echo $RESPONSE | jq -r '.build.status') + + if [ "$STATUS" = "finished" ]; then + SUCCESS=$(echo $RESPONSE | jq -r '.success') + BUILD_URL=$(echo $RESPONSE | jq -r '.buildUrl') + echo "status=$STATUS" >> $GITHUB_OUTPUT + echo "success=$SUCCESS" >> $GITHUB_OUTPUT + echo "build_url=$BUILD_URL" >> $GITHUB_OUTPUT + break + elif [ "$STATUS" = "failed" ]; then + echo "status=failed" >> $GITHUB_OUTPUT + break + fi + + sleep 60 + done + + - name: Slack Notification + uses: 8398a7/action-slack@v3 + if: always() + with: + status: ${{ steps.check_status.outputs.success == 'true' && 'success' || 'failure' }} + fields: repo,message,commit,author,action,eventName,ref,workflow,job,took + text: | + Mobile CI Build Result + Branch: ${{ github.event.inputs.branch }} + Workflow: ${{ github.event.inputs.workflow_id }} + Build URL: ${{ steps.check_status.outputs.build_url }} + env: + SLACK_WEBHOOK_URL: ${{ secrets.RELEASE_SLACK_WEBHOOK }} diff --git a/codemagic.yaml b/codemagic.yaml new file mode 100644 index 000000000000..3e592adb7cdc --- /dev/null +++ b/codemagic.yaml @@ -0,0 +1,47 @@ +workflows: + ios-workflow: + name: iOS Workflow + instance_type: mac_mini_m2 + max_build_duration: 30 + environment: + flutter: 3.22.3 + xcode: latest + cocoapods: default + + scripts: + - name: Build Flutter + script: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + source "$HOME/.cargo/env" + rustc --version + cargo --version + + cd frontend + + rustup target install aarch64-apple-ios-sim + cargo install --force cargo-make + cargo install --force duckscript_cli + cargo install --force cargo-lipo + + cargo make appflowy-flutter-deps-tools + cargo make --profile development-ios-arm64-sim appflowy-core-dev-ios + cargo make --profile development-ios-arm64-sim code_generation + + - name: iOS integration tests + script: | + cd frontend/appflowy_flutter + flutter emulators --launch apple_ios_simulator + flutter -d iPhone test integration_test/runner.dart + + artifacts: + - build/ios/ipa/*.ipa + - /tmp/xcodebuild_logs/*.log + - flutter_drive.log + + publishing: + email: + recipients: + - lucas.xu@appflowy.io + notify: + success: true + failure: true diff --git a/frontend/appflowy_flutter/integration_test/mobile/document/page_style_test.dart b/frontend/appflowy_flutter/integration_test/mobile/document/page_style_test.dart index 6e94eed1b84a..67d2b05d9d57 100644 --- a/frontend/appflowy_flutter/integration_test/mobile/document/page_style_test.dart +++ b/frontend/appflowy_flutter/integration_test/mobile/document/page_style_test.dart @@ -36,7 +36,7 @@ import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('document page style', () { + group('document page style:', () { double getCurrentEditorFontSize() { final editorPage = find .byType(AppFlowyEditorPage) @@ -57,11 +57,9 @@ void main() { .single .widget as AppFlowyEditorPage; return editorPage.styleCustomizer - .style() - .textStyleConfiguration - .text - .height ?? - PageStyleLineHeightLayout.normal.lineHeight; + .style() + .textStyleConfiguration + .lineHeight; } testWidgets('change font size in page style settings', (tester) async { @@ -87,20 +85,24 @@ void main() { await tester.openPage(gettingStarted); // click the layout button await tester.tapButton(find.byType(MobileViewPageLayoutButton)); + var lineHeight = getCurrentEditorLineHeight(); expect( - getCurrentEditorLineHeight(), + lineHeight, PageStyleLineHeightLayout.normal.lineHeight, ); // change line height from normal to large await tester.tapSvgButton(FlowySvgs.m_layout_large_s); + await tester.pumpAndSettle(); + lineHeight = getCurrentEditorLineHeight(); expect( - getCurrentEditorLineHeight(), + lineHeight, PageStyleLineHeightLayout.large.lineHeight, ); // change line height from large to small await tester.tapSvgButton(FlowySvgs.m_layout_small_s); + lineHeight = getCurrentEditorLineHeight(); expect( - getCurrentEditorLineHeight(), + lineHeight, PageStyleLineHeightLayout.small.lineHeight, ); }); diff --git a/frontend/appflowy_flutter/integration_test/mobile/home_page/create_new_page_test.dart b/frontend/appflowy_flutter/integration_test/mobile/home_page/create_new_page_test.dart index ae4e5ddea51f..a10052926abc 100644 --- a/frontend/appflowy_flutter/integration_test/mobile/home_page/create_new_page_test.dart +++ b/frontend/appflowy_flutter/integration_test/mobile/home_page/create_new_page_test.dart @@ -29,19 +29,18 @@ import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('create new page', () { + group('create new page in home page:', () { testWidgets('create document', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.local, - ); + await tester.launchInAnonymousMode(); // tap the create page button final createPageButton = find.byWidgetPredicate( (widget) => widget is FlowySvg && - widget.svg.path == FlowySvgs.m_home_unselected_m.path, + widget.svg.path == FlowySvgs.m_home_add_m.path, ); await tester.tapButton(createPageButton); + await tester.pumpAndSettle(); expect(find.byType(MobileDocumentScreen), findsOneWidget); }); }); diff --git a/frontend/appflowy_flutter/integration_test/mobile/sign_in/anonymous_sign_in_test.dart b/frontend/appflowy_flutter/integration_test/mobile/sign_in/anonymous_sign_in_test.dart index 427e554733bf..1a0493b6a744 100644 --- a/frontend/appflowy_flutter/integration_test/mobile/sign_in/anonymous_sign_in_test.dart +++ b/frontend/appflowy_flutter/integration_test/mobile/sign_in/anonymous_sign_in_test.dart @@ -25,9 +25,9 @@ import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('anonymous sign in on mobile', () { + group('anonymous sign in on mobile:', () { testWidgets('anon user and then sign in', (tester) async { - await tester.initializeAppFlowy(); + await tester.launchInAnonymousMode(); // expect to see the home page expect(find.byType(MobileHomeScreen), findsOneWidget); diff --git a/frontend/appflowy_flutter/integration_test/mobile_runner.dart b/frontend/appflowy_flutter/integration_test/mobile_runner_1.dart similarity index 81% rename from frontend/appflowy_flutter/integration_test/mobile_runner.dart rename to frontend/appflowy_flutter/integration_test/mobile_runner_1.dart index 3f47f83997a1..bf86020141c4 100644 --- a/frontend/appflowy_flutter/integration_test/mobile_runner.dart +++ b/frontend/appflowy_flutter/integration_test/mobile_runner_1.dart @@ -5,8 +5,13 @@ import 'mobile/document/page_style_test.dart' as page_style_test; import 'mobile/home_page/create_new_page_test.dart' as create_new_page_test; import 'mobile/sign_in/anonymous_sign_in_test.dart' as anonymous_sign_in_test; -Future runIntegrationOnMobile() async { +Future main() async { Log.shared.disableLog = true; + + await runIntegration1OnMobile(); +} + +Future runIntegration1OnMobile() async { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); anonymous_sign_in_test.main(); diff --git a/frontend/appflowy_flutter/integration_test/runner.dart b/frontend/appflowy_flutter/integration_test/runner.dart index d995f81f6dcf..247c23323630 100644 --- a/frontend/appflowy_flutter/integration_test/runner.dart +++ b/frontend/appflowy_flutter/integration_test/runner.dart @@ -8,7 +8,7 @@ import 'desktop_runner_5.dart'; import 'desktop_runner_6.dart'; import 'desktop_runner_7.dart'; import 'desktop_runner_8.dart'; -import 'mobile_runner.dart'; +import 'mobile_runner_1.dart'; /// The main task runner for all integration tests in AppFlowy. /// @@ -28,7 +28,7 @@ Future main() async { await runIntegration7OnDesktop(); await runIntegration8OnDesktop(); } else if (Platform.isIOS || Platform.isAndroid) { - await runIntegrationOnMobile(); + await runIntegration1OnMobile(); } else { throw Exception('Unsupported platform'); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart index b6c23c3d8c94..4d4eecbc1d72 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart @@ -65,7 +65,10 @@ class _MobileViewPageState extends State { @override void dispose() { _appBarOpacity.dispose(); - _scrollNotificationObserver?.removeListener(_onScrollNotification); + + // there's no need to remove the listener, because the observer will be disposed when the widget is unmounted. + // inside the observer, the listener will be removed automatically. + // _scrollNotificationObserver?.removeListener(_onScrollNotification); _scrollNotificationObserver = null; super.dispose(); diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 27ee616618f2..42554d59c238 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -1543,10 +1543,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: "direct dev" description: @@ -1941,10 +1941,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" string_validator: dependency: "direct main" description: @@ -2246,10 +2246,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.5" watcher: dependency: transitive description: