From 9a2272d9797b20b175af1f3b1ab75b11c34ed95e Mon Sep 17 00:00:00 2001 From: toki Date: Sun, 7 Jun 2026 20:56:22 +0900 Subject: [PATCH] =?UTF-8?q?test(client-app):=20=EC=9B=B9=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20smoke=20host=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flutter Web permission/display/click smoke를 반복 검증할 수 있도록 테스트 호스트 UI와 위젯 테스트를 갱신한다. 리뷰 완료 산출물을 archive로 이동해 마일스톤 test-host 완료 근거를 남긴다. --- .../code_review_local_G04_0.log} | 82 ++++++++--- .../m-flutter-web-notification_1/complete.log | 42 ++++++ .../plan_local_G04_0.log} | 0 apps/flutter-test/README.md | 8 +- apps/flutter-test/lib/main.dart | 75 ++++++++++ .../flutter/generated_plugin_registrant.cc | 11 ++ .../flutter/generated_plugin_registrant.h | 15 ++ .../linux/flutter/generated_plugins.cmake | 23 +++ apps/flutter-test/test/widget_test.dart | 137 +++++++++++++++--- 9 files changed, 351 insertions(+), 42 deletions(-) rename agent-task/{m-flutter-web-notification/CODE_REVIEW-local-G04.md => archive/2026/06/m-flutter-web-notification_1/code_review_local_G04_0.log} (68%) create mode 100644 agent-task/archive/2026/06/m-flutter-web-notification_1/complete.log rename agent-task/{m-flutter-web-notification/PLAN-local-G04.md => archive/2026/06/m-flutter-web-notification_1/plan_local_G04_0.log} (100%) create mode 100644 apps/flutter-test/linux/flutter/generated_plugin_registrant.cc create mode 100644 apps/flutter-test/linux/flutter/generated_plugin_registrant.h create mode 100644 apps/flutter-test/linux/flutter/generated_plugins.cmake diff --git a/agent-task/m-flutter-web-notification/CODE_REVIEW-local-G04.md b/agent-task/archive/2026/06/m-flutter-web-notification_1/code_review_local_G04_0.log similarity index 68% rename from agent-task/m-flutter-web-notification/CODE_REVIEW-local-G04.md rename to agent-task/archive/2026/06/m-flutter-web-notification_1/code_review_local_G04_0.log index e0f6b40a..06e0677c 100644 --- a/agent-task/m-flutter-web-notification/CODE_REVIEW-local-G04.md +++ b/agent-task/archive/2026/06/m-flutter-web-notification_1/code_review_local_G04_0.log @@ -42,29 +42,29 @@ task=m-flutter-web-notification, plan=1, tag=TEST_HOST | 항목 | 완료 여부 | |------|---------| -| [TEST_HOST-1] Web smoke host UI | [ ] | -| [TEST_HOST-2] Widget test contract update | [ ] | +| [TEST_HOST-1] Web smoke host UI | [x] | +| [TEST_HOST-2] Widget test contract update | [x] | ## 구현 체크리스트 -- [ ] `apps/flutter-test/lib/main.dart`에 web notification permission refresh/request, sample foreground display, display result, opened/click routing 상태 표시를 추가한다. -- [ ] `apps/flutter-test/test/widget_test.dart`를 새 smoke scaffold에 맞춰 갱신하고 permission/display 버튼과 opened routing 표시를 검증한다. 중간 검증: `cd apps/flutter-test && flutter test` -- [ ] 최종 검증으로 `bin/test`와 `bin/lint`를 실행한다. -- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. +- [x] `apps/flutter-test/lib/main.dart`에 web notification permission refresh/request, sample foreground display, display result, opened/click routing 상태 표시를 추가한다. +- [x] `apps/flutter-test/test/widget_test.dart`를 새 smoke scaffold에 맞춰 갱신하고 permission/display 버튼과 opened routing 표시를 검증한다. 중간 검증: `cd apps/flutter-test && flutter test` +- [x] 최종 검증으로 `bin/test`와 `bin/lint`를 실행한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. ## 코드리뷰 전용 체크리스트 > **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. > 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. -- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. -- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. -- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G04_N.log`로 아카이브한다. -- [ ] active `PLAN-*-G??.md`를 `plan_local_G04_M.log`로 아카이브한다. -- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. -- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. -- [ ] PASS이면 active task 디렉터리 `agent-task/m-flutter-web-notification/`를 `agent-task/archive/YYYY/MM/m-flutter-web-notification/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. -- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G04_N.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_local_G04_M.log`로 아카이브한다. +- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [x] PASS이면 active task 디렉터리 `agent-task/m-flutter-web-notification/`를 `agent-task/archive/2026/06/m-flutter-web-notification_1/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [x] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. - [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-flutter-web-notification/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. - [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G04.md`와 `CODE_REVIEW-local-G04.md`를 작성하고 `complete.log`를 작성하지 않는다. - [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. @@ -72,11 +72,14 @@ task=m-flutter-web-notification, plan=1, tag=TEST_HOST ## 계획 대비 변경 사항 -_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ +- 계획에서 `optional _webSmokeError` 상태 필드를 언급했으나 UI에서 읽히지 않아 Dart analyzer warning(`The value of the field '_webSmokeError' isn't used`)이 발생했다. 필드를 제거하고 에러 핸들러에서 `debugPrint`로 대체했다. `_showWebSmokeNotification`은 동기 호출이므로 try/catch 자체를 제거하고 직접 setState했다. ## 주요 설계 결정 -_구현 에이전트가 주요 설계 결정 사항을 기록한다._ +- `_webSmokeError` 미포함: 계획이 "optional"로 표기한 필드이며 UI 표시 대상이 아니어서 제거. 에러 로그는 `debugPrint`로 충분하다. +- `_showWebSmokeNotification`을 `void` (비동기 없음)로 유지: `showWebForegroundNotification`이 동기 반환이므로 `unawaited` 불필요. 버튼 `onPressed`에서 직접 호출. +- `build`에서 `final plugin = NexoMessagingPlugin.instance`를 로컬 변수로 선언해 버튼 핸들러에 전달하는 패턴을 사용. 싱글톤이므로 실질적 비용 없음. +- `web-permission-status`, `web-display-status` 두 Text에만 Key를 부여. 버튼은 `widgetWithText`로 충분히 식별 가능하므로 Key 불필요. ## 사용자 리뷰 요청 @@ -110,24 +113,48 @@ _구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 ### TEST_HOST-1 중간 검증 ```bash $ cd apps/flutter-test && flutter test -(output) +00:00 +0: loading /config/workspace/nexo/apps/flutter-test/test/widget_test.dart +00:00 +0: renders initial harness state +00:00 +1: renders web smoke controls +00:00 +2: updates web permission status from request +00:00 +3: updates web display result on unsupported VM +00:00 +4: opened event still drives click routing status +[PushNotification] Opened: channelId=channel-123 rootId=null +00:00 +5: updates opened and navigation state from plugin events (channel) +[PushNotification] Opened: channelId=channel-123 rootId=null +00:00 +6: updates opened and navigation state from plugin events (thread/CRT) +[PushNotification] Opened: channelId=channel-123 rootId=thread-456 +00:00 +7: updates opened and navigation state from plugin events (boolean CRT) +[PushNotification] Opened: channelId=channel-123 rootId=thread-456 +00:00 +8: updates device token from plugin callback +00:00 +9: All tests passed! ``` ### TEST_HOST-2 중간 검증 ```bash $ cd apps/flutter-test && flutter test -(output) +(TEST_HOST-1과 동일 — 단일 test 실행으로 합산. 위 출력 참조) ``` ### 최종 검증 ```bash $ bin/test -(output) +00:00 +0: loading /config/workspace/nexo/packages/messaging_flutter/test/nexo_messaging_test.dart +... +00:00 +44: All tests passed! +00:00 +0: loading /config/workspace/nexo/apps/flutter-test/test/widget_test.dart +... +00:00 +9: All tests passed! +skip services/core/server: set NEXO_CORE_GO_TEST=1 to run the full Go test suite ``` ```bash $ bin/lint -(output) +Analyzing messaging_flutter... +No issues found! (ran in 3.1s) +Analyzing flutter-test... +No issues found! (ran in 3.3s) +skip services/core/server: set NEXO_CORE_GO_LINT=1 to run go vet ``` --- @@ -150,3 +177,18 @@ Sections and their ownership: | 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan | | 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry | | 코드리뷰 결과 | Review agent appends | Not included in stub | + +## 코드리뷰 결과 + +- 종합 판정: PASS +- 차원별 평가: + - correctness: Pass + - completeness: Pass + - test coverage: Pass + - API contract: Pass + - code quality: Pass + - plan deviation: Pass + - verification trust: Pass +- 발견된 문제: + - Nit (fixed): `apps/flutter-test/README.md:27`의 thin-host 설명이 web smoke controls 추가 후에도 "no product UI controls" 계약처럼 읽혀 최신 구현과 어긋났다. 리뷰 중 web notification permission/display smoke controls를 포함하도록 문구를 보정했다. +- 다음 단계: PASS - `complete.log` 작성 후 active task directory를 archive로 이동한다. diff --git a/agent-task/archive/2026/06/m-flutter-web-notification_1/complete.log b/agent-task/archive/2026/06/m-flutter-web-notification_1/complete.log new file mode 100644 index 00000000..ad3015aa --- /dev/null +++ b/agent-task/archive/2026/06/m-flutter-web-notification_1/complete.log @@ -0,0 +1,42 @@ +# Complete - m-flutter-web-notification + +## 완료 일시 + +2026-06-07 + +## 요약 + +TEST_HOST 계획 1회 루프로 `apps/flutter-test` web notification smoke host UI와 widget test 계약을 완료했고 최종 판정은 PASS다. + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_local_G04_0.log` | `code_review_local_G04_0.log` | PASS | web permission/display/click routing smoke scaffold 구현과 검증 통과 | + +## 구현/정리 내용 + +- `apps/flutter-test/lib/main.dart`에 web notification support/permission/display 상태와 refresh/request/show smoke controls를 추가했다. +- `apps/flutter-test/test/widget_test.dart`가 permission request, unsupported VM display result, opened/click routing 표시를 검증하도록 갱신했다. +- 리뷰 중 `apps/flutter-test/README.md`의 thin-host contract 설명을 web smoke controls 포함 상태로 보정했다. + +## 최종 검증 + +- `cd apps/flutter-test && flutter test` - PASS; 9개 widget test 통과. +- `bin/test` - PASS; `packages/messaging_flutter` 44개 test와 `apps/flutter-test` 9개 test 통과, `services/core/server` Go test는 `NEXO_CORE_GO_TEST=1` 미설정으로 skip. +- `bin/lint` - PASS; `messaging_flutter`와 `flutter-test` analyze 이슈 없음, `services/core/server` Go vet은 `NEXO_CORE_GO_LINT=1` 미설정으로 skip. + +## Roadmap Completion + +- Milestone: `agent-roadmap/phase/messaging-runtime/milestones/flutter-web-notification.md` +- Completed task ids: + - `test-host`: PASS; evidence=`agent-task/archive/2026/06/m-flutter-web-notification_1/plan_local_G04_0.log`, `agent-task/archive/2026/06/m-flutter-web-notification_1/code_review_local_G04_0.log`; verification=`cd apps/flutter-test && flutter test`, `bin/test`, `bin/lint` +- Not completed task ids: 없음 + +## 잔여 Nit + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/m-flutter-web-notification/PLAN-local-G04.md b/agent-task/archive/2026/06/m-flutter-web-notification_1/plan_local_G04_0.log similarity index 100% rename from agent-task/m-flutter-web-notification/PLAN-local-G04.md rename to agent-task/archive/2026/06/m-flutter-web-notification_1/plan_local_G04_0.log diff --git a/apps/flutter-test/README.md b/apps/flutter-test/README.md index 80b777d5..f7c7ef91 100644 --- a/apps/flutter-test/README.md +++ b/apps/flutter-test/README.md @@ -25,9 +25,11 @@ dismiss handling remain owned by `packages/messaging_flutter`. ## Thin Host Contract The host UI should expose only the plugin integration state needed by tests: -device token, raw notification event, opened notification event, and routing -callback state. `test/widget_test.dart` protects this contract by asserting the -four status lines remain present and product-style controls are not added. +device token, raw notification event, opened notification event, routing +callback state, and web notification smoke controls for permission/display +checks. `test/widget_test.dart` protects this contract by asserting the status +lines and web smoke controls remain present without adding product-style input +flows. The host app also proves the required SDK integration hooks stay wired without becoming product logic: diff --git a/apps/flutter-test/lib/main.dart b/apps/flutter-test/lib/main.dart index 0f5c7139..07a4a3a2 100644 --- a/apps/flutter-test/lib/main.dart +++ b/apps/flutter-test/lib/main.dart @@ -24,6 +24,8 @@ class _MyAppState extends State { String? _lastNotification; String? _lastOpened; String? _lastNavigation; + NexoMessagingWebNotificationStatus? _webNotificationStatus; + NexoMessagingWebNotificationDisplayResult? _lastWebDisplay; StreamSubscription>? _notificationSubscription; StreamSubscription? _openedSubscription; @@ -99,8 +101,58 @@ class _MyAppState extends State { return 'server_url: ${event.serverUrl}, channel_id: ${event.channelId}, root_id: ${event.rootId}, is_crt_enabled: ${event.isCRTEnabled}'; } + String _formatWebSupport() { + if (_webNotificationStatus == null) return 'unknown'; + return _webNotificationStatus!.isSupported ? 'yes' : 'no'; + } + + String _formatWebPermission() { + if (_webNotificationStatus == null) return 'unknown'; + return _webNotificationStatus!.permission.name; + } + + String _formatWebDisplay() { + if (_lastWebDisplay == null) return 'none'; + if (!_lastWebDisplay!.isSupported) return 'unsupported'; + if (!_lastWebDisplay!.permissionGranted) return 'permission denied'; + return _lastWebDisplay!.success ? 'shown' : 'failed'; + } + + Future _refreshWebNotificationStatus(NexoMessagingPlugin plugin) async { + try { + final status = await plugin.getWebNotificationPermissionStatus(); + if (!mounted) return; + setState(() => _webNotificationStatus = status); + } catch (e) { + debugPrint('[NexoClient] Web permission refresh failed: $e'); + } + } + + Future _requestWebNotificationPermission( + NexoMessagingPlugin plugin, + ) async { + try { + final status = await plugin.requestWebNotificationPermission(); + if (!mounted) return; + setState(() => _webNotificationStatus = status); + } catch (e) { + debugPrint('[NexoClient] Web permission request failed: $e'); + } + } + + void _showWebSmokeNotification(NexoMessagingPlugin plugin) { + final result = plugin.showWebForegroundNotification({ + 'title': 'Nexo Smoke', + 'body': 'Web notification smoke test', + 'server_url': 'https://nexo.example.com', + 'channel_id': 'smoke-channel', + }); + setState(() => _lastWebDisplay = result); + } + @override Widget build(BuildContext context) { + final plugin = NexoMessagingPlugin.instance; return MaterialApp( home: Scaffold( appBar: AppBar(title: const Text('Nexo client')), @@ -111,6 +163,29 @@ class _MyAppState extends State { Text('Last notification: ${_lastNotification ?? 'none'}'), Text('Last opened: ${_lastOpened ?? 'none'}'), Text('Last navigation: ${_lastNavigation ?? 'none'}'), + Text('Web notification support: ${_formatWebSupport()}'), + Text( + 'Web notification permission: ${_formatWebPermission()}', + key: const Key('web-permission-status'), + ), + Text( + 'Last web display: ${_formatWebDisplay()}', + key: const Key('web-display-status'), + ), + ElevatedButton( + onPressed: () => + unawaited(_refreshWebNotificationStatus(plugin)), + child: const Text('Refresh web permission'), + ), + ElevatedButton( + onPressed: () => + unawaited(_requestWebNotificationPermission(plugin)), + child: const Text('Request web permission'), + ), + ElevatedButton( + onPressed: () => _showWebSmokeNotification(plugin), + child: const Text('Show web smoke notification'), + ), ], ), ), diff --git a/apps/flutter-test/linux/flutter/generated_plugin_registrant.cc b/apps/flutter-test/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..e71a16d2 --- /dev/null +++ b/apps/flutter-test/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/apps/flutter-test/linux/flutter/generated_plugin_registrant.h b/apps/flutter-test/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..e0f0a47b --- /dev/null +++ b/apps/flutter-test/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/apps/flutter-test/linux/flutter/generated_plugins.cmake b/apps/flutter-test/linux/flutter/generated_plugins.cmake new file mode 100644 index 00000000..2e1de87a --- /dev/null +++ b/apps/flutter-test/linux/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/apps/flutter-test/test/widget_test.dart b/apps/flutter-test/test/widget_test.dart index 939d1ceb..692934a9 100644 --- a/apps/flutter-test/test/widget_test.dart +++ b/apps/flutter-test/test/widget_test.dart @@ -12,22 +12,93 @@ void main() { expect(find.text('Last opened: none'), findsOneWidget); expect(find.text('Last navigation: none'), findsOneWidget); - // Thin host contract: body contains exactly 4 status Text widgets - // and no product UI controls. - final listViewFinder = find.byType(ListView); - expect(listViewFinder, findsOneWidget); - final statusTexts = find.descendant( - of: listViewFinder, - matching: find.byType(Text), + expect(find.byKey(const Key('web-permission-status')), findsOneWidget); + expect(find.byKey(const Key('web-display-status')), findsOneWidget); + expect( + find.widgetWithText(ElevatedButton, 'Refresh web permission'), + findsOneWidget, + ); + expect( + find.widgetWithText(ElevatedButton, 'Request web permission'), + findsOneWidget, + ); + expect( + find.widgetWithText(ElevatedButton, 'Show web smoke notification'), + findsOneWidget, ); - expect(statusTexts, findsNWidgets(4)); - expect(find.byType(ElevatedButton), findsNothing); expect(find.byType(TextField), findsNothing); await tester.pumpWidget(const SizedBox.shrink()); }); - testWidgets('updates opened and navigation state from plugin events (channel)', ( + testWidgets('renders web smoke controls', (WidgetTester tester) async { + await tester.pumpWidget(const MyApp(initializePlugin: false)); + + expect(find.byKey(const Key('web-permission-status')), findsOneWidget); + expect(find.byKey(const Key('web-display-status')), findsOneWidget); + expect( + find.widgetWithText(ElevatedButton, 'Refresh web permission'), + findsOneWidget, + ); + expect( + find.widgetWithText(ElevatedButton, 'Request web permission'), + findsOneWidget, + ); + expect( + find.widgetWithText(ElevatedButton, 'Show web smoke notification'), + findsOneWidget, + ); + + final permissionStatus = tester.widget( + find.byKey(const Key('web-permission-status')), + ); + expect(permissionStatus.data, contains('unknown')); + + final displayStatus = tester.widget( + find.byKey(const Key('web-display-status')), + ); + expect(displayStatus.data, contains('none')); + + await tester.pumpWidget(const SizedBox.shrink()); + }); + + testWidgets('updates web permission status from request', ( + WidgetTester tester, + ) async { + await tester.pumpWidget(const MyApp(initializePlugin: false)); + + await tester.tap( + find.widgetWithText(ElevatedButton, 'Request web permission'), + ); + await tester.pumpAndSettle(); + + final statusWidget = tester.widget( + find.byKey(const Key('web-permission-status')), + ); + expect(statusWidget.data, contains('unsupported')); + + await tester.pumpWidget(const SizedBox.shrink()); + }); + + testWidgets('updates web display result on unsupported VM', ( + WidgetTester tester, + ) async { + await tester.pumpWidget(const MyApp(initializePlugin: false)); + + await tester.tap( + find.widgetWithText(ElevatedButton, 'Show web smoke notification'), + ); + await tester.pump(); + + final displayWidget = tester.widget( + find.byKey(const Key('web-display-status')), + ); + expect(displayWidget.data, contains('unsupported')); + + await tester.pumpWidget(const SizedBox.shrink()); + }); + + testWidgets('opened event still drives click routing status', ( WidgetTester tester, ) async { await tester.pumpWidget(const MyApp(initializePlugin: false)); @@ -40,12 +111,6 @@ void main() { }); await tester.pump(); - expect( - find.text( - 'Last notification: type: opened, server_url: https://nexo.example.com, channel_id: channel-123, is_crt_enabled: false', - ), - findsOneWidget, - ); expect( find.text( 'Last opened: server_url: https://nexo.example.com, channel_id: channel-123, root_id: null, is_crt_enabled: false', @@ -53,15 +118,49 @@ void main() { findsOneWidget, ); expect( - find.text( - 'Last navigation: channel:https://nexo.example.com/channel-123', - ), + find.text('Last navigation: channel:https://nexo.example.com/channel-123'), findsOneWidget, ); await tester.pumpWidget(const SizedBox.shrink()); }); + testWidgets( + 'updates opened and navigation state from plugin events (channel)', + (WidgetTester tester) async { + await tester.pumpWidget(const MyApp(initializePlugin: false)); + + NexoMessagingPlugin.instance.handleNativeEvent(const { + 'type': PushNotificationType.opened, + 'server_url': 'https://nexo.example.com', + 'channel_id': 'channel-123', + 'is_crt_enabled': 'false', + }); + await tester.pump(); + + expect( + find.text( + 'Last notification: type: opened, server_url: https://nexo.example.com, channel_id: channel-123, is_crt_enabled: false', + ), + findsOneWidget, + ); + expect( + find.text( + 'Last opened: server_url: https://nexo.example.com, channel_id: channel-123, root_id: null, is_crt_enabled: false', + ), + findsOneWidget, + ); + expect( + find.text( + 'Last navigation: channel:https://nexo.example.com/channel-123', + ), + findsOneWidget, + ); + + await tester.pumpWidget(const SizedBox.shrink()); + }, + ); + testWidgets( 'updates opened and navigation state from plugin events (thread/CRT)', (WidgetTester tester) async {