diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 3ba4b5e..1168453 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,11 +8,13 @@ import Foundation import cloud_firestore import firebase_auth import firebase_core +import patrol import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + PatrolPlugin.register(with: registry.registrar(forPlugin: "PatrolPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/patrol_test/web_smoke_test.dart b/patrol_test/web_smoke_test.dart new file mode 100644 index 0000000..aba9e02 --- /dev/null +++ b/patrol_test/web_smoke_test.dart @@ -0,0 +1,65 @@ +import 'package:boxmatch/main.dart' as app; +import 'package:flutter_test/flutter_test.dart'; +import 'package:patrol/patrol.dart'; + +void main() { + patrolTest('web smoke flow on chrome', ($) async { + app.main(); + await $.pumpAndTrySettle(timeout: const Duration(seconds: 10)); + + final tester = $.tester; + var homeVisible = false; + try { + await $('展場剩食媒合').waitUntilVisible( + timeout: const Duration(seconds: 20), + ); + homeVisible = true; + } catch (_) { + try { + await $('Exhibition Surplus Matching').waitUntilVisible( + timeout: const Duration(seconds: 20), + ); + homeVisible = true; + } catch (_) { + homeVisible = false; + } + } + expect(homeVisible, isTrue, reason: 'Home page title is not visible after launch'); + + final launchError = tester.takeException(); + expect(launchError, isNull, reason: 'Unexpected exception during app launch'); + + final mapLabels = ['地圖', '場館地圖', 'Map', 'Venue map']; + var mapTapped = false; + for (final label in mapLabels) { + try { + await $(label).tap(); + mapTapped = true; + break; + } catch (_) { + // try next label + } + } + expect(mapTapped, isTrue, reason: 'Cannot find map tab label in current locale'); + await $.pumpAndTrySettle(timeout: const Duration(seconds: 10)); + + var mapVisible = false; + for (final label in ['場館地圖', 'Venue map']) { + try { + await $(label).waitUntilVisible(timeout: const Duration(seconds: 20)); + mapVisible = true; + break; + } catch (_) { + // try next label + } + } + expect(mapVisible, isTrue, reason: 'Map page did not render expected title'); + + final navigationError = tester.takeException(); + expect( + navigationError, + isNull, + reason: 'Unexpected exception after navigating to map page', + ); + }); +} diff --git a/playwright-report/data/a9507836fa978016534c05fb18798fbaf8b9dade.md b/playwright-report/data/a9507836fa978016534c05fb18798fbaf8b9dade.md new file mode 100644 index 0000000..fc1c78c --- /dev/null +++ b/playwright-report/data/a9507836fa978016534c05fb18798fbaf8b9dade.md @@ -0,0 +1,32 @@ +# Page snapshot + +```yaml +- generic [ref=e4]: + - generic: + - generic: + - generic: + - generic: + - generic: + - generic: + - generic: + - heading "展場剩食媒合" [active] [level=2] + - button "我的預約" [ref=e5] + - button "重新整理" [ref=e6] + - button "Language 繁中" [ref=e7] + - generic: + - generic: 在公開展場快速媒合,減少浪費。 + - generic: + - generic: 平台聲明:Boxmatch 僅提供媒合,不保證食品安全。 + - checkbox "全部場館" [checked] [ref=e8] + - checkbox "僅收藏場館" [ref=e9] + - checkbox "附近場館" [ref=e10] + - checkbox "可立即取餐" [ref=e11] + - generic: + - generic: "僅收藏場館: 0" + - button "地圖" [ref=e12] + - generic: + - group: + - generic: + - generic: 目前沒有可領取項目。 可切到地圖查看,或由企業先發佈。 + - button "去場館地圖看看" [ref=e13] +``` \ No newline at end of file diff --git a/playwright-report/index.html b/playwright-report/index.html new file mode 100644 index 0000000..f39f404 --- /dev/null +++ b/playwright-report/index.html @@ -0,0 +1,85 @@ + + + + + + + + + Playwright Test Report + + + + +
+ + + \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index c2430b0..b5b9d4f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -113,6 +113,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + dispose_scope: + dependency: transitive + description: + name: dispose_scope + sha256: "48ec38ca2631c53c4f8fa96b294c801e55c335db5e3fb9f82cede150cfe5a2af" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + equatable: + dependency: transitive + description: + name: equatable + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" + url: "https://pub.dev" + source: hosted + version: "2.0.8" fake_async: dependency: transitive description: @@ -256,6 +272,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.20.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + url: "https://pub.dev" + source: hosted + version: "4.11.0" latlong2: dependency: "direct main" description: @@ -392,6 +416,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + patrol: + dependency: "direct dev" + description: + name: patrol + sha256: "7825a6e96a8f0755f68eec600a91a08b19bd0975488a70885b3696f6b65ffc0f" + url: "https://pub.dev" + source: hosted + version: "4.5.0" + patrol_finders: + dependency: transitive + description: + name: patrol_finders + sha256: "9970eac0669a90b20ec7e1bcaabd0475655655998068ca656f4df9f6ec84f336" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + patrol_log: + dependency: transitive + description: + name: patrol_log + sha256: a2360db165c34692665c0de146e5157887d6b584fdccca8f141f947a5acf1b2e + url: "https://pub.dev" + source: hosted + version: "0.8.0" platform: dependency: transitive description: @@ -480,6 +528,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 3bf08d5..c459c46 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,6 +56,7 @@ dev_dependencies: # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^6.0.0 + patrol: ^4.5.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000..c59fa77 --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,6 @@ +{ + "status": "failed", + "failedTests": [ + "e0e5c6321cedaeb39e74-b1370df8a2f4e3bca926" + ] +} \ No newline at end of file diff --git a/test-results/tests-test-web-smoke-test-web-smoke-flow-on-chrome/error-context.md b/test-results/tests-test-web-smoke-test-web-smoke-flow-on-chrome/error-context.md new file mode 100644 index 0000000..fc1c78c --- /dev/null +++ b/test-results/tests-test-web-smoke-test-web-smoke-flow-on-chrome/error-context.md @@ -0,0 +1,32 @@ +# Page snapshot + +```yaml +- generic [ref=e4]: + - generic: + - generic: + - generic: + - generic: + - generic: + - generic: + - generic: + - heading "展場剩食媒合" [active] [level=2] + - button "我的預約" [ref=e5] + - button "重新整理" [ref=e6] + - button "Language 繁中" [ref=e7] + - generic: + - generic: 在公開展場快速媒合,減少浪費。 + - generic: + - generic: 平台聲明:Boxmatch 僅提供媒合,不保證食品安全。 + - checkbox "全部場館" [checked] [ref=e8] + - checkbox "僅收藏場館" [ref=e9] + - checkbox "附近場館" [ref=e10] + - checkbox "可立即取餐" [ref=e11] + - generic: + - generic: "僅收藏場館: 0" + - button "地圖" [ref=e12] + - generic: + - group: + - generic: + - generic: 目前沒有可領取項目。 可切到地圖查看,或由企業先發佈。 + - button "去場館地圖看看" [ref=e13] +``` \ No newline at end of file diff --git a/test_bundle.dart b/test_bundle.dart new file mode 100644 index 0000000..0c208ee --- /dev/null +++ b/test_bundle.dart @@ -0,0 +1,89 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND AND DO NOT COMMIT TO VERSION CONTROL +// ignore_for_file: type=lint, invalid_use_of_internal_member + +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:patrol/patrol.dart'; +import 'package:patrol/src/platform/contracts/contracts.dart'; +import 'package:test_api/src/backend/invoker.dart'; + +// START: GENERATED TEST IMPORTS +import 'patrol_test/web_smoke_test.dart' as web_smoke_test; +// END: GENERATED TEST IMPORTS + +Future main() async { + // This is the entrypoint of the bundled Dart test. + // + // Its responsibilities are: + // * Running a special Dart test that runs before all the other tests and + // explores the hierarchy of groups and tests. + // * Hosting a PatrolAppService, which the native side of Patrol uses to get + // the Dart tests, and to request execution of a specific Dart test. + // + // When running on Android, the Android Test Orchestrator, before running the + // tests, makes an initial run to gather the tests that it will later run. The + // native side of Patrol (specifically: PatrolJUnitRunner class) is hooked + // into the Android Test Orchestrator lifecycle and knows when that initial + // run happens. When it does, PatrolJUnitRunner makes an RPC call to + // PatrolAppService and asks it for Dart tests. + // + // When running on iOS, the native side of Patrol (specifically: the + // PATROL_INTEGRATION_TEST_IOS_RUNNER macro) makes an initial run to gather + // the tests that it will later run (same as the Android). During that initial + // run, it makes an RPC call to PatrolAppService and asks it for Dart tests. + // + // Once the native runner has the list of Dart tests, it dynamically creates + // native test cases from them. On Android, this is done using the + // Parametrized JUnit runner. On iOS, new test case methods are swizzled into + // the RunnerUITests class, taking advantage of the very dynamic nature of + // Objective-C runtime. + // + // Execution of these dynamically created native test cases is then fully + // managed by the underlying native test framework (JUnit on Android, XCTest + // on iOS). The native test cases do only one thing - request execution of the + // Dart test (out of which they had been created) and wait for it to complete. + // The result of running the Dart test is the result of the native test case. + + final platformAutomator = PlatformAutomator( + config: PlatformAutomatorConfig.defaultConfig(), + ); + await platformAutomator.initialize(); + final binding = PatrolBinding.ensureInitialized(platformAutomator); + final testExplorationCompleter = Completer(); + + // A special test to explore the hierarchy of groups and tests. This is a hack + // around https://github.com/dart-lang/test/issues/1998. + // + // This test must be the first to run. If not, the native side likely won't + // receive any tests, and everything will fall apart. + test('patrol_test_explorer', () { + // Maybe somewhat counterintuitively, this callback runs *after* the calls + // to group() below. + final topLevelGroup = Invoker.current!.liveTest.groups.first; + final dartTestGroup = createDartTestGroup( + topLevelGroup, + tags: null, + excludeTags: null, + ); + testExplorationCompleter.complete(dartTestGroup); + print('patrol_test_explorer: obtained Dart-side test hierarchy:'); + reportGroupStructure(dartTestGroup); + }); + +// START: GENERATED TEST GROUPS + group('web_smoke_test', web_smoke_test.main); +// END: GENERATED TEST GROUPS + + final dartTestGroup = await testExplorationCompleter.future; + final appService = PatrolAppService(topLevelDartTestGroup: dartTestGroup); + binding.patrolAppService = appService; + await runAppService(appService); + + // Until now, the native test runner was waiting for us, the Dart side, to + // come alive. Now that we did, let's tell it that we're ready to be asked + // about Dart tests. + await platformAutomator.markPatrolAppServiceReady(); + + await appService.testExecutionCompleted; +}