Skip to content

Commit 363881e

Browse files
FeodorFitsnerInesaFitsnerOwenMcDonnell
authored
v1: Controls integration tests - part I (#5546)
* Checkbox basic test and PopupMenuTheme docstrings * add cupertino action sheet test * Checkbox tests * removed screenshot from example test * Add tests * Fix tests * add simple app-bar test * try counter app test from examples folder * Tests cleanup * move test_app_bar.py and reference image * add dropdown test * Add macOS integration tests GitHub Actions workflow Introduces a new workflow to run integration tests on macOS 14 using CocoaPods, uv, and Flutter. The workflow installs dependencies, runs Python integration tests, and uploads failure screenshots as artifacts. * Update macOS CI to use flutter-action and set version Replaces manual Flutter installation with subosito/flutter-action, sets FLUTTER_VERSION to 3.32.8, and simplifies the workflow. This improves maintainability and ensures consistent Flutter setup in CI. * Disable integration tests on appveyor * Update macos-integration-tests.yml * Button, ElevatedButton, FilledButton tests * FloatingActionButton test * IconButton * Add page screenshot capability and update dropdown test Implemented the ability to take full-page screenshots in the Flutter page control and exposed it via the Python BasePage API. Updated the dropdown integration test to use the new screenshot feature and replaced golden images accordingly. Also clarified the integration test README for golden image generation. * banner test * PopupMenuButton basic test * OutlinedButton basic test * MenuItemButton basic test * SegmentedButton basic test * SubmenuButton basic test Also fixed content type * CupertinoButton basic test * CupertinoActionSheetAction basic test * CupertinoContextMenuAction basic test * CupertinoDialogAction basic test * CupertinoFilledButton basic test * CupertinoSegmentedButton basic test * CupertinoSlidingSegmentedButton basic test * CupertinoTintedButton basic test * TextButton basic test * Add canvas drawing tests and golden images for macOS Introduces integration tests for canvas drawing features including color fill, points (as points, lines, polygon), and shadow rendering. Adds corresponding golden images for macOS to validate visual output. * Simplify canvas shadow test * CircleAvatar test * Skip test_draw_shadow in canvas integration tests Temporarily disables the test_draw_shadow test in test_canvas.py using pytest.mark.skip, likely due to instability or pending fixes. * AlertDialog basic test * BottomSheet basic test * CupertinoAlertDialog basic test * CupertinoBottomSheet basic test * Add long press support to Tester API Introduces a longPress method to the Tester interface and its implementations in Dart and Python, enabling long press interactions in automated tests. Updates the service layer to handle the new 'long_press' command. * CupertinoContextMenu not done, menu doesn't open * Image test --------- Co-authored-by: InesaFitsner <[email protected]> Co-authored-by: Owen McDonnell <[email protected]>
1 parent 555ad7e commit 363881e

File tree

97 files changed

+1115
-19
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

97 files changed

+1115
-19
lines changed

.appveyor.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ environment:
2626
job_group: build_flet_package
2727
APPVEYOR_BUILD_WORKER_IMAGE: ubuntu2204
2828

29-
- job_name: Integration tests on macOS
30-
job_group: integration_tests
31-
python_stack: python 3.10
32-
APPVEYOR_BUILD_WORKER_IMAGE: macos-sonoma
33-
FLET_TEST_SCREENSHOTS_PIXEL_RATIO: 2.0
34-
FLET_TEST_SCREENSHOTS_SIMILARITY_THRESHOLD: 99.0
35-
FLET_TEST_DISABLE_FVM: 1
29+
# - job_name: Integration tests on macOS
30+
# job_group: integration_tests
31+
# python_stack: python 3.10
32+
# APPVEYOR_BUILD_WORKER_IMAGE: macos-sonoma
33+
# FLET_TEST_SCREENSHOTS_PIXEL_RATIO: 2.0
34+
# FLET_TEST_SCREENSHOTS_SIMILARITY_THRESHOLD: 99.0
35+
# FLET_TEST_DISABLE_FVM: 1
3636

3737
- job_name: Build Flet for Windows
3838
job_group: build_flet
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: macOS Integration Tests
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
env:
8+
FLUTTER_VERSION: "3.32.8"
9+
FLET_TEST_SCREENSHOTS_PIXEL_RATIO: "2.0"
10+
FLET_TEST_SCREENSHOTS_SIMILARITY_THRESHOLD: "99.0"
11+
FLET_TEST_DISABLE_FVM: "1"
12+
13+
jobs:
14+
test-macos:
15+
runs-on: macos-14
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Install CocoaPods
20+
run: brew install cocoapods
21+
22+
- name: Setup Flutter
23+
uses: subosito/flutter-action@v2
24+
with:
25+
flutter-version: ${{ env.FLUTTER_VERSION }}
26+
channel: stable
27+
cache: true
28+
29+
- name: Install uv
30+
shell: bash
31+
run: |
32+
curl -LsSf https://astral.sh/uv/install.sh | sh
33+
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
34+
35+
- name: Show tool versions
36+
run: |
37+
python3 --version
38+
uv --version
39+
pod --version
40+
flutter --version
41+
42+
- name: Run integration tests
43+
working-directory: sdk/python
44+
run: |
45+
uv run pytest -s -o log_cli=true -o log_cli_level=INFO packages/flet/integration_tests
46+
47+
- name: Upload failure screenshots
48+
if: failure()
49+
uses: actions/upload-artifact@v4
50+
with:
51+
name: integration-test-failures-macos
52+
path: sdk/python/packages/flet/integration_tests/**/*_actual.png
53+
if-no-files-found: ignore

client/integration_test/flutter_tester.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ class FlutterWidgetTester implements Tester {
7474
Future<void> tap(TestFinder finder) =>
7575
_tester.tap((finder as FlutterTestFinder).raw);
7676

77+
@override
78+
Future<void> longPress(TestFinder finder) =>
79+
_tester.longPress((finder as FlutterTestFinder).raw);
80+
7781
@override
7882
Future<void> enterText(TestFinder finder, String text) =>
7983
_tester.enterText((finder as FlutterTestFinder).raw, text);

packages/flet/lib/src/controls/page.dart

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import 'dart:ui' as ui;
12
import 'dart:ui';
23

34
import 'package:collection/collection.dart';
45
import 'package:flutter/cupertino.dart';
56
import 'package:flutter/foundation.dart';
67
import 'package:flutter/material.dart';
8+
import 'package:flutter/rendering.dart';
79
import 'package:flutter/services.dart';
810
import 'package:flutter_localizations/flutter_localizations.dart';
911
import 'package:provider/provider.dart';
@@ -25,6 +27,7 @@ import '../utils/platform_utils_web.dart'
2527
import '../utils/session_store_web.dart'
2628
if (dart.library.io) "../utils/session_store_non_web.dart";
2729
import '../utils/theme.dart';
30+
import '../utils/time.dart';
2831
import '../utils/user_fonts.dart';
2932
import '../widgets/animated_transition_page.dart';
3033
import '../widgets/loading_page.dart';
@@ -44,6 +47,7 @@ class PageControl extends StatefulWidget {
4447

4548
class _PageControlState extends State<PageControl> with WidgetsBindingObserver {
4649
final _navigatorKey = GlobalKey<NavigatorState>();
50+
final _rootKey = GlobalKey();
4751
late final RouteState _routeState;
4852
late final SimpleRouterDelegate _routerDelegate;
4953
late final RouteParser _routeParser;
@@ -52,7 +56,7 @@ class _PageControlState extends State<PageControl> with WidgetsBindingObserver {
5256
ServiceRegistry? _userServices;
5357
bool? _prevOnKeyboardEvent;
5458
bool _keyboardHandlerSubscribed = false;
55-
59+
double _dpr = 1.0;
5660
String? _prevViewRoutes;
5761

5862
final Map<int, MultiView> _multiViews = <int, MultiView>{};
@@ -87,13 +91,14 @@ class _PageControlState extends State<PageControl> with WidgetsBindingObserver {
8791
onRestart: () => _handleAppLifecycleTransition('restart'));
8892

8993
_attachKeyboardListenerIfNeeded();
94+
widget.control.addInvokeMethodListener(_invokeMethod);
9095
}
9196

9297
@override
9398
void didChangeDependencies() {
9499
debugPrint("Page.didChangeDependencies: ${widget.control.id}");
95100
super.didChangeDependencies();
96-
101+
_dpr = MediaQuery.devicePixelRatioOf(context);
97102
_loadFontsIfNeeded(FletBackend.of(context));
98103
}
99104

@@ -148,9 +153,30 @@ class _PageControlState extends State<PageControl> with WidgetsBindingObserver {
148153
if (_keyboardHandlerSubscribed) {
149154
HardwareKeyboard.instance.removeHandler(_handleKeyDown);
150155
}
156+
widget.control.removeInvokeMethodListener(_invokeMethod);
151157
super.dispose();
152158
}
153159

160+
Future<dynamic> _invokeMethod(String name, dynamic args) async {
161+
debugPrint("Page.$name($args)");
162+
switch (name) {
163+
case "take_screenshot":
164+
if (_rootKey.currentContext == null) {
165+
return null;
166+
}
167+
await Future.delayed(
168+
parseDuration(args["delay"], const Duration(milliseconds: 20))!);
169+
final boundary = _rootKey.currentContext!.findRenderObject()
170+
as RenderRepaintBoundary;
171+
final image = await boundary.toImage(
172+
pixelRatio: parseDouble(args["pixel_ratio"], _dpr)!);
173+
final data = await image.toByteData(format: ui.ImageByteFormat.png);
174+
return data!.buffer.asUint8List();
175+
default:
176+
throw Exception("Unknown Page method: $name");
177+
}
178+
}
179+
154180
void _updateMultiViews() {
155181
if (!widget.control.backend.multiView) {
156182
return;
@@ -383,7 +409,7 @@ class _PageControlState extends State<PageControl> with WidgetsBindingObserver {
383409
var showSemanticsDebugger =
384410
control.getBool("show_semantics_debugger", false)!;
385411

386-
var app = widgetsDesign == PageDesign.cupertino
412+
Widget? app = widgetsDesign == PageDesign.cupertino
387413
? home != null
388414
? CupertinoApp(
389415
debugShowCheckedModeBanner: false,
@@ -432,6 +458,14 @@ class _PageControlState extends State<PageControl> with WidgetsBindingObserver {
432458
supportedLocales: localeConfiguration.supportedLocales,
433459
locale: localeConfiguration.locale,
434460
);
461+
462+
if (control.getBool("enable_screenshots") == true) {
463+
app = RepaintBoundary(
464+
key: _rootKey,
465+
child: app,
466+
);
467+
}
468+
435469
return PageContext(
436470
themeMode: themeMode,
437471
brightness: brightness,

packages/flet/lib/src/controls/submenu_button.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ class _SubmenuButtonControlState extends State<SubmenuButtonControl> {
8282
leadingIcon: widget.control.buildWidget("leading"),
8383
trailingIcon: widget.control.buildWidget("trailing"),
8484
menuChildren: widget.control.buildWidgets("controls"),
85-
child: widget.control.buildWidget("content"),
85+
child: widget.control.buildTextOrWidget("content"),
8686
);
8787

8888
var focusValue = widget.control.getString("focus");

packages/flet/lib/src/services/tester.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ class TesterService extends FletService {
7979
await control.backend.tester!.tap(finder);
8080
}
8181

82+
case "long_press":
83+
var finder = _finders[args["id"]];
84+
if (finder != null) {
85+
await control.backend.tester!.longPress(finder);
86+
}
87+
8288
case "enter_text":
8389
var finder = _finders[args["id"]];
8490
if (finder != null) {

packages/flet/lib/src/testing/tester.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ abstract class Tester {
1414
TestFinder findByIcon(IconData icon);
1515
Future<Uint8List> takeScreenshot(String name);
1616
Future<void> tap(TestFinder finder);
17+
Future<void> longPress(TestFinder finder);
1718
Future<void> enterText(TestFinder finder, String text);
1819
Future<void> mouseHover(TestFinder finder);
1920
void teardown();

sdk/python/examples/apps/counter/counter.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def plus_click(e):
1818
page.add(
1919
ft.Row(
2020
[
21-
ft.IconButton(ft.Icons.REMOVE, on_click=minus_click),
21+
ft.IconButton(ft.Icons.REMOVE, on_click=minus_click, key="decrement"),
2222
txt_number,
2323
ft.IconButton(ft.Icons.ADD, on_click=plus_click),
2424
],
@@ -27,4 +27,5 @@ def plus_click(e):
2727
)
2828

2929

30-
ft.run(main)
30+
if __name__ == "__main__":
31+
ft.run(main)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from pathlib import Path
2+
3+
import counter as app
4+
import flet as ft
5+
import flet.testing as ftt
6+
import pytest
7+
import pytest_asyncio
8+
9+
10+
@pytest_asyncio.fixture(scope="module")
11+
async def flet_app(request):
12+
flet_app = ftt.FletTestApp(
13+
flutter_app_dir=(Path(__file__).parent / "../../../../../client").resolve(),
14+
flet_app_main=app.main,
15+
test_path=request.fspath,
16+
)
17+
await flet_app.start()
18+
yield flet_app
19+
await flet_app.teardown()
20+
21+
22+
@pytest.mark.asyncio(loop_scope="module")
23+
async def test_app(flet_app: ftt.FletTestApp):
24+
tester = flet_app.tester
25+
await tester.pump_and_settle()
26+
zero_text = await tester.find_by_text("0")
27+
assert zero_text.count == 1
28+
29+
# tap increment button
30+
increment_btn = await tester.find_by_icon(ft.Icons.ADD)
31+
assert increment_btn.count == 1
32+
await tester.tap(increment_btn)
33+
await tester.pump_and_settle()
34+
assert (await tester.find_by_text("1")).count == 1
35+
36+
# tap decrement button
37+
decrement_button = await tester.find_by_key("decrement")
38+
assert decrement_button.count == 1
39+
await tester.tap(decrement_button)
40+
await tester.tap(decrement_button)
41+
await tester.pump_and_settle()
42+
assert (await tester.find_by_text("-1")).count == 1

sdk/python/examples/controls/app_bar/actions_and_popup_menu.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,5 @@ def handle_checked_item_click(e: ft.Event[ft.PopupMenuItem]):
3333
page.add(ft.Text("Body!"))
3434

3535

36-
ft.run(main)
36+
if __name__ == "__main__":
37+
ft.run(main)

0 commit comments

Comments
 (0)