웹 브라우저: MKV 화면과 오가도 보던 페이지 · 세션 · 뒤로 가기 기록 유지 (앱을 새로 켤 때까지)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>main
parent
34ddd57e62
commit
8c03efd489
|
|
@ -0,0 +1,82 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
import 'package:jj_mkvmaker/app/app_controller.dart';
|
||||||
|
import 'package:jj_mkvmaker/app/bookmarks_controller.dart';
|
||||||
|
import 'package:jj_mkvmaker/app/download_manager.dart';
|
||||||
|
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
||||||
|
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
|
||||||
|
import 'package:jj_mkvmaker/services/platform_services.dart';
|
||||||
|
import 'package:jj_mkvmaker/ui/browser_page.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
/// 실제 Edge(WebView2): MKV 화면 ↔ 브라우저를 여러 번 오가도 보던 페이지를 다시 읽지 않고 그대로 이어진다
|
||||||
|
void main() {
|
||||||
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
testWidgets('MKV 화면 ↔ 브라우저: 페이지 · 세션 유지', (tester) async {
|
||||||
|
var hits = 0;
|
||||||
|
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
||||||
|
server.listen((req) async {
|
||||||
|
if (req.uri.path == '/page') hits++;
|
||||||
|
req.response
|
||||||
|
..headers.contentType = ContentType.html
|
||||||
|
..write('<html><head><meta charset="utf-8"><title>JJ 유지</title></head><body>유지</body></html>');
|
||||||
|
await req.response.close();
|
||||||
|
});
|
||||||
|
final url = 'http://127.0.0.1:${server.port}/page';
|
||||||
|
final dataDir = Directory.systemTemp.createTempSync('jj_keep_').path;
|
||||||
|
|
||||||
|
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||||
|
c.settings
|
||||||
|
..webViewDataDir = dataDir
|
||||||
|
..homeUrl = url;
|
||||||
|
final bm = BookmarksController(p.join(dataDir, 'bm.json'));
|
||||||
|
final d = DownloadManager(backends: const [], settings: () => c.settings, readClipboard: () async => null);
|
||||||
|
final navKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
navigatorKey: navKey,
|
||||||
|
navigatorObservers: [browserRouteObserver],
|
||||||
|
home: const Scaffold(body: Text('MKV 화면')),
|
||||||
|
));
|
||||||
|
Future<void> wait(int ms) async {
|
||||||
|
await tester.runAsync(() => Future<void>.delayed(Duration(milliseconds: ms)));
|
||||||
|
await tester.pump();
|
||||||
|
}
|
||||||
|
|
||||||
|
String address() {
|
||||||
|
final f = find.byType(TextField);
|
||||||
|
return f.evaluate().isEmpty ? '' : tester.widget<TextField>(f.first).controller!.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 브라우저를 열어 페이지를 읽는다
|
||||||
|
BrowserPage.open(navKey.currentState!, c: c, downloads: d, bookmarks: bm);
|
||||||
|
for (var i = 0; i < 100 && hits == 0; i++) {
|
||||||
|
await wait(100);
|
||||||
|
}
|
||||||
|
await wait(1000);
|
||||||
|
expect(hits, 1);
|
||||||
|
|
||||||
|
for (var round = 1; round <= 3; round++) {
|
||||||
|
// MKV 화면으로 (브라우저 화면이 닫힌다) → 다시 브라우저
|
||||||
|
navKey.currentState!.popUntil((r) => r.isFirst);
|
||||||
|
for (var i = 0; i < 10; i++) {
|
||||||
|
await wait(100);
|
||||||
|
}
|
||||||
|
expect(find.text('MKV 화면'), findsOneWidget);
|
||||||
|
BrowserPage.open(navKey.currentState!, c: c, downloads: d, bookmarks: bm);
|
||||||
|
for (var i = 0; i < 25; i++) {
|
||||||
|
await wait(100);
|
||||||
|
}
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('RESULT $round번째: 주소 ${address()} · 페이지 요청 $hits번 · 브라우저 ${find.byType(BrowserPage).evaluate().length} · '
|
||||||
|
'입력칸 ${find.byType(TextField).evaluate().length} · MKV ${find.text('MKV 화면').evaluate().length}');
|
||||||
|
expect(address(), url, reason: '$round번째: 보던 주소');
|
||||||
|
expect(hits, 1, reason: '$round번째: 페이지를 다시 읽지 않음 (같은 웹뷰)');
|
||||||
|
}
|
||||||
|
await server.close(force: true);
|
||||||
|
}, timeout: const Timeout(Duration(minutes: 3)));
|
||||||
|
}
|
||||||
|
|
@ -87,6 +87,13 @@ class BrowserPage extends StatefulWidget {
|
||||||
/// 열려 있는 브라우저 화면 수
|
/// 열려 있는 브라우저 화면 수
|
||||||
static int _open = 0;
|
static int _open = 0;
|
||||||
|
|
||||||
|
/// 마지막으로 보던 주소: MKV 화면으로 갔다가 브라우저를 다시 열면 홈이 아니라 이 주소로
|
||||||
|
static String? _lastUrl;
|
||||||
|
|
||||||
|
/// 앱 안 웹뷰를 화면이 닫혀도 살려 둔다 (다시 열면 보던 페이지 · 재생 위치 · 뒤로 가기 기록 그대로)
|
||||||
|
static final _keepAlive = InAppWebViewKeepAlive();
|
||||||
|
static bool _keepAliveUsed = false;
|
||||||
|
|
||||||
/// 웹 브라우저로: 이미 열린 브라우저가 있으면 그 화면으로 돌아가고 (보던 페이지 그대로),
|
/// 웹 브라우저로: 이미 열린 브라우저가 있으면 그 화면으로 돌아가고 (보던 페이지 그대로),
|
||||||
/// 없으면 새로 연다. 다운로드 목록 · 플레이어 등에서 브라우저 버튼을 눌러도 보던 페이지를 잃지 않는다.
|
/// 없으면 새로 연다. 다운로드 목록 · 플레이어 등에서 브라우저 버튼을 눌러도 보던 페이지를 잃지 않는다.
|
||||||
static Future<void> open(NavigatorState nav,
|
static Future<void> open(NavigatorState nav,
|
||||||
|
|
@ -107,7 +114,7 @@ class BrowserPage extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BrowserPageState extends State<BrowserPage> with RouteAware {
|
class _BrowserPageState extends State<BrowserPage> with RouteAware {
|
||||||
late String _url = widget.initialUrl ?? widget.c.settings.homeUrl;
|
late String _url = widget.initialUrl ?? BrowserPage._lastUrl ?? widget.c.settings.homeUrl;
|
||||||
String _title = '';
|
String _title = '';
|
||||||
double _progress = 1;
|
double _progress = 1;
|
||||||
bool _hasVideo = false, _canBack = false, _canFwd = false, _panel = false;
|
bool _hasVideo = false, _canBack = false, _canFwd = false, _panel = false;
|
||||||
|
|
@ -123,6 +130,7 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
|
||||||
late final BrowserHost _host = BrowserHost(
|
late final BrowserHost _host = BrowserHost(
|
||||||
onUrl: (u) {
|
onUrl: (u) {
|
||||||
if (!mounted || u.isEmpty) return;
|
if (!mounted || u.isEmpty) return;
|
||||||
|
BrowserPage._lastUrl = u;
|
||||||
setState(() => _url = u);
|
setState(() => _url = u);
|
||||||
if (!_addressFocus.hasFocus) _address.text = u;
|
if (!_addressFocus.hasFocus) _address.text = u;
|
||||||
},
|
},
|
||||||
|
|
@ -160,6 +168,12 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
|
||||||
_nav?.pauseMedia();
|
_nav?.pauseMedia();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// MKV 화면 등으로 가며 브라우저 화면이 닫힌다: 살려 둔 웹뷰에서 소리만 나지 않게 멈춘다
|
||||||
|
@override
|
||||||
|
void didPop() {
|
||||||
|
_nav?.pauseMedia();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
browserRouteObserver.unsubscribe(this);
|
browserRouteObserver.unsubscribe(this);
|
||||||
|
|
@ -449,6 +463,24 @@ Future<WebViewEnvironment?> _createEnvironment(String dir) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _EdgeViewState extends State<_EdgeView> {
|
class _EdgeViewState extends State<_EdgeView> {
|
||||||
|
/// 살려 둔 웹뷰를 이 화면이 쓰는지 (브라우저 화면은 하나뿐이지만, 혹시 둘이면 둘째는 새 웹뷰)
|
||||||
|
InAppWebViewKeepAlive? _keepAlive;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (!BrowserPage._keepAliveUsed) {
|
||||||
|
BrowserPage._keepAliveUsed = true;
|
||||||
|
_keepAlive = BrowserPage._keepAlive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (_keepAlive != null) BrowserPage._keepAliveUsed = false;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => FutureBuilder<WebViewEnvironment?>(
|
Widget build(BuildContext context) => FutureBuilder<WebViewEnvironment?>(
|
||||||
future: browserEnvironment(widget.dataDir),
|
future: browserEnvironment(widget.dataDir),
|
||||||
|
|
@ -472,6 +504,7 @@ class _EdgeViewState extends State<_EdgeView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return InAppWebView(
|
return InAppWebView(
|
||||||
|
keepAlive: _keepAlive,
|
||||||
webViewEnvironment: snap.data,
|
webViewEnvironment: snap.data,
|
||||||
initialUrlRequest: URLRequest(url: WebUri(normalizeAddress(widget.initialUrl))),
|
initialUrlRequest: URLRequest(url: WebUri(normalizeAddress(widget.initialUrl))),
|
||||||
initialSettings: InAppWebViewSettings(
|
initialSettings: InAppWebViewSettings(
|
||||||
|
|
@ -479,7 +512,15 @@ class _EdgeViewState extends State<_EdgeView> {
|
||||||
mediaPlaybackRequiresUserGesture: false,
|
mediaPlaybackRequiresUserGesture: false,
|
||||||
supportMultipleWindows: false,
|
supportMultipleWindows: false,
|
||||||
),
|
),
|
||||||
onWebViewCreated: (c) => h.attach(_InAppNav(c, snap.data, widget.dataDir)),
|
onWebViewCreated: (c) async {
|
||||||
|
h.attach(_InAppNav(c, snap.data, widget.dataDir));
|
||||||
|
// 살려 둔 웹뷰를 다시 붙였으면 보던 주소 · 제목 · 뒤로 가기 상태를 다시 알린다
|
||||||
|
final u = await c.getUrl();
|
||||||
|
if (u != null) h.onUrl(u.toString());
|
||||||
|
final t = await c.getTitle();
|
||||||
|
if (t != null) h.onTitle(t);
|
||||||
|
await check(c);
|
||||||
|
},
|
||||||
onLoadStart: (c, u) {
|
onLoadStart: (c, u) {
|
||||||
h.onUrl(u?.toString() ?? '');
|
h.onUrl(u?.toString() ?? '');
|
||||||
h.onVideo(false);
|
h.onVideo(false);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue