test(client-app): 웹 알림 smoke host를 추가한다
Flutter Web permission/display/click smoke를 반복 검증할 수 있도록 테스트 호스트 UI와 위젯 테스트를 갱신한다. 리뷰 완료 산출물을 archive로 이동해 마일스톤 test-host 완료 근거를 남긴다.
This commit is contained in:
parent
9017482f4b
commit
9a2272d979
9 changed files with 351 additions and 42 deletions
|
|
@ -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-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, 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-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, 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로 이동한다.
|
||||
|
|
@ -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
|
||||
|
||||
- 없음
|
||||
|
||||
## 후속 작업
|
||||
|
||||
- 없음
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ class _MyAppState extends State<MyApp> {
|
|||
String? _lastNotification;
|
||||
String? _lastOpened;
|
||||
String? _lastNavigation;
|
||||
NexoMessagingWebNotificationStatus? _webNotificationStatus;
|
||||
NexoMessagingWebNotificationDisplayResult? _lastWebDisplay;
|
||||
StreamSubscription<Map<String, dynamic>>? _notificationSubscription;
|
||||
StreamSubscription<NotificationOpenedEvent>? _openedSubscription;
|
||||
|
||||
|
|
@ -99,8 +101,58 @@ class _MyAppState extends State<MyApp> {
|
|||
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<void> _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<void> _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<MyApp> {
|
|||
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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||
#define GENERATED_PLUGIN_REGISTRANT_
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
|
||||
// Registers Flutter plugins.
|
||||
void fl_register_plugins(FlPluginRegistry* registry);
|
||||
|
||||
#endif // GENERATED_PLUGIN_REGISTRANT_
|
||||
23
apps/flutter-test/linux/flutter/generated_plugins.cmake
Normal file
23
apps/flutter-test/linux/flutter/generated_plugins.cmake
Normal file
|
|
@ -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 $<TARGET_FILE:${plugin}_plugin>)
|
||||
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)
|
||||
|
|
@ -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<Text>(
|
||||
find.byKey(const Key('web-permission-status')),
|
||||
);
|
||||
expect(permissionStatus.data, contains('unknown'));
|
||||
|
||||
final displayStatus = tester.widget<Text>(
|
||||
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<Text>(
|
||||
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<Text>(
|
||||
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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue