250 고침: 덮개를 띄우면 웹 뷰가 새로 만들어져 덮개가 바로 걷히던 것 · 엔진의 본문 오류로도 판단
a4 의 931 기기 확인 (노트 10+): DNS 192.0.2.1 에서 브라우저에 덮개가 보이지 않고 ERR_TUNNEL_CONNECTION_FAILED 만 (버그 수정).
- 원인: 덮개가 없을 때는 웹 뷰를, 있을 때는 Stack 안의 웹 뷰를 돌려 트리 모양이 바뀌었다 → 웹 뷰가 새로 만들어져
페이지를 다시 읽고, 다시 읽기 시작에서 덮개가 걷혔다. Windows 실제 Edge 통합 시험에서 재현
("WindowsInAppWebViewController was used after being disposed"). 늘 같은 Stack 모양으로.
- 엔진이 알려 주는 본문 오류 (onReceivedError, Edge · Android) 도 받아 판단한다: 그 이름의 실패 기록이 있거나,
DNS 서버에 지금 닿지 않는 중에 프록시 터널 실패 (TUNNEL · PROXY) 면 덮개. 오류 뒤에 오는 읽기 끝이 덮개를 지우지 않는다.
DNS 강제 중의 본문 오류는 작업 기록에 남긴다 (기기에서 원인을 볼 수 있게).
- 시험: 덮개를 띄워도 웹 뷰를 다시 만들지 않음 · 본문 오류 경로 · DNS 와 상관없는 오류는 엔진 화면 그대로.
실제 Edge: integration_test/dns_block_real_test.dart (덮개 → [다시 시도] → 다시 덮개) 통과.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
통합
parent
614584c78c
commit
0b0f2617ae
|
|
@ -1568,6 +1568,7 @@
|
|||
"웹 브라우저의 시작 · 🏠 주소": "Start · 🏠 page of the web browser",
|
||||
"웹 페이지 자동 번역": "Translate web pages automatically",
|
||||
"웹 페이지를 번역하지 못했습니다: {0}": "Could not translate the web page: {0}",
|
||||
"웹 페이지를 열지 못함 ({0}): {1}": "Couldn't open web page ({0}): {1}",
|
||||
"위 ← 아래": "Top ← bottom",
|
||||
"위 → 아래": "Top → bottom",
|
||||
"위 ⇄ 아래": "Top ⇄ bottom",
|
||||
|
|
|
|||
|
|
@ -1568,6 +1568,7 @@
|
|||
"웹 브라우저의 시작 · 🏠 주소": "ウェブブラウザーの開始・🏠 ページ",
|
||||
"웹 페이지 자동 번역": "ウェブページを自動翻訳",
|
||||
"웹 페이지를 번역하지 못했습니다: {0}": "ウェブページを翻訳できませんでした: {0}",
|
||||
"웹 페이지를 열지 못함 ({0}): {1}": "Web ページを開けませんでした ({0}): {1}",
|
||||
"위 ← 아래": "上 ← 下",
|
||||
"위 → 아래": "上 → 下",
|
||||
"위 ⇄ 아래": "上 ⇄ 下",
|
||||
|
|
|
|||
|
|
@ -1568,6 +1568,7 @@
|
|||
"웹 브라우저의 시작 · 🏠 주소": "网页浏览器的起始 · 🏠 页面",
|
||||
"웹 페이지 자동 번역": "自动翻译网页",
|
||||
"웹 페이지를 번역하지 못했습니다: {0}": "无法翻译网页: {0}",
|
||||
"웹 페이지를 열지 못함 ({0}): {1}": "无法打开网页 ({0}):{1}",
|
||||
"위 ← 아래": "上 ← 下",
|
||||
"위 → 아래": "上 → 下",
|
||||
"위 ⇄ 아래": "上 ⇄ 下",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
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/net_guard.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/app_actions.dart';
|
||||
import 'package:jj_mkvmaker/ui/browser_page.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// 250 실제 엔진 (Windows Edge WebView2): DNS 강제 서버에 닿지 않으면 (TEST-NET 192.0.2.1) 엔진의 오류 화면
|
||||
/// (net::ERR_TUNNEL_CONNECTION_FAILED) 대신 앱의 덮개 "DNS 서버 (…) 에 닿지 않아 … 을(를) 열 수 없습니다" 가 뜨는지.
|
||||
/// 데이터는 JJ_MKVMAKER_DATA (없으면 임시 폴더) 에만 - 사용자 앱 데이터를 건드리지 않는다.
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
testWidgets('250: 닿지 않는 DNS → 브라우저에 앱 덮개 (Edge 실제)', (tester) async {
|
||||
final data = Platform.environment['JJ_MKVMAKER_DATA'] ?? Directory.systemTemp.createTempSync('jj_dns_block_').path;
|
||||
Directory(data).createSync(recursive: true);
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..webViewDataDir = p.join(data, 'webview')
|
||||
..dnsMode = 'custom'
|
||||
..dnsPrimary = '192.0.2.1'
|
||||
..dnsSecondary = '';
|
||||
final logs = <String>[];
|
||||
final g = NetGuard.instance
|
||||
..config = (() => c.settings.dnsConfig)
|
||||
..log = logs.add;
|
||||
await tester.runAsync(g.ensure);
|
||||
Future<void> wait(int ms) async {
|
||||
for (var i = 0; i < ms ~/ 200; i++) {
|
||||
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 200)));
|
||||
await tester.pump();
|
||||
}
|
||||
}
|
||||
|
||||
final bm = BookmarksController(p.join(data, 'bm.json'));
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
builder: (context, child) => AppScope(controller: c, bookmarks: bm, child: child!),
|
||||
home: BrowserPage(c: c, bookmarks: bm, initialUrl: 'https://example.org/'),
|
||||
));
|
||||
final cover = find.textContaining('에 닿지 않아 example.org 을(를) 열 수 없습니다');
|
||||
for (var i = 0; i < 90 && cover.evaluate().isEmpty; i++) {
|
||||
await wait(400);
|
||||
}
|
||||
// ignore: avoid_print
|
||||
print('RESULT 덮개: ${cover.evaluate().isNotEmpty} / 알림: ${g.failure.value?.server} / 기록: ${c.logs.where((l) => l.contains('웹 페이지')).toList()}');
|
||||
expect(cover, findsOneWidget);
|
||||
expect(find.text('시스템 DNS 로 바꾸기'), findsWidgets);
|
||||
expect(find.text('다시 시도'), findsOneWidget);
|
||||
|
||||
// [다시 시도]: 덮개를 걷고 다시 읽는다 → 여전히 닿지 않으니 다시 덮개
|
||||
await tester.tap(find.text('다시 시도'));
|
||||
await tester.pump();
|
||||
expect(cover, findsNothing);
|
||||
for (var i = 0; i < 90 && cover.evaluate().isEmpty; i++) {
|
||||
await wait(400);
|
||||
}
|
||||
// ignore: avoid_print
|
||||
print('RESULT 다시 시도 뒤 덮개: ${cover.evaluate().isNotEmpty}');
|
||||
expect(cover, findsOneWidget);
|
||||
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
await wait(1000);
|
||||
g.config = () => c.settings.dnsConfig;
|
||||
});
|
||||
}
|
||||
|
|
@ -77,6 +77,9 @@ class BrowserHost {
|
|||
final void Function(bool hasVideo) onVideo;
|
||||
final void Function(bool back, bool forward) onHistory;
|
||||
final void Function(WebNav nav) attach;
|
||||
|
||||
/// 250: 본문 (main frame) 을 읽지 못함 - 엔진이 알려 준 주소와 오류 (예: net::ERR_TUNNEL_CONNECTION_FAILED)
|
||||
final void Function(String url, String error)? onMainFrameError;
|
||||
const BrowserHost({
|
||||
required this.onUrl,
|
||||
required this.onTitle,
|
||||
|
|
@ -84,6 +87,7 @@ class BrowserHost {
|
|||
required this.onVideo,
|
||||
required this.onHistory,
|
||||
required this.attach,
|
||||
this.onMainFrameError,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -161,17 +165,26 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
|
|||
(String, String)? _dnsBlocked;
|
||||
DateTime _loadStartAt = DateTime.now();
|
||||
|
||||
void _checkDnsBlocked() {
|
||||
final host = Uri.tryParse(_url)?.host ?? '';
|
||||
final server = host.isEmpty ? null : NetGuard.instance.dnsUnreachableFor(host, _loadStartAt);
|
||||
final next = server == null ? null : (host, server);
|
||||
/// [error]: 엔진이 알려 준 본문 오류. 프록시 터널 실패이고 DNS 서버에 지금 닿지 않는 중이면, 그 이름의 실패 기록이 없어도
|
||||
/// (Chromium 이 실패한 프록시를 기억해 다시 묻지 않는 때 등) DNS 때문으로 본다.
|
||||
/// 덮개는 여기서 걷지 않는다 (새로 읽기 시작 · [다시 시도] 에서) - Android 는 오류 뒤에 읽기 끝이 또 온다.
|
||||
void _checkDnsBlocked({String? host, String? error}) {
|
||||
final h = host ?? Uri.tryParse(_url)?.host ?? '';
|
||||
if (h.isEmpty) return;
|
||||
final g = NetGuard.instance;
|
||||
var server = g.dnsUnreachableFor(h, _loadStartAt);
|
||||
final f = g.failure.value;
|
||||
if (server == null && f != null && error != null && (error.contains('TUNNEL') || error.contains('PROXY'))) server = f.server;
|
||||
if (server == null) return;
|
||||
final next = (h, server);
|
||||
if (next != _dnsBlocked) setState(() => _dnsBlocked = next);
|
||||
}
|
||||
|
||||
/// 250: DNS 실패면 웹 뷰 위에 덮는다 ([시스템 DNS 로 바꾸기] [다시 시도])
|
||||
/// 250: DNS 실패면 웹 뷰 위에 덮는다 ([시스템 DNS 로 바꾸기] [다시 시도]).
|
||||
/// 트리 모양은 늘 같게 (Stack 첫 칸이 웹 뷰) - 덮개를 띄울 때 모양이 바뀌면 웹 뷰가 새로 만들어져 페이지를 다시 읽고,
|
||||
/// 다시 읽기가 시작되며 덮개가 걷혔다 (931 노트 10+ 에서 덮개가 보이지 않던 것, Windows 실제 시험에서 재현)
|
||||
Widget _withDnsBlock(Widget view) {
|
||||
final b = _dnsBlocked;
|
||||
if (b == null) return view;
|
||||
void retry() {
|
||||
setState(() => _dnsBlocked = null);
|
||||
_nav?.reload();
|
||||
|
|
@ -179,6 +192,7 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
|
|||
|
||||
return Stack(children: [
|
||||
Positioned.fill(child: view),
|
||||
if (b != null)
|
||||
Positioned.fill(
|
||||
child: ColoredBox(
|
||||
color: JjColors.bg,
|
||||
|
|
@ -259,6 +273,15 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
|
|||
});
|
||||
},
|
||||
attach: (n) => _nav = BrowserPage.debugNav = n,
|
||||
// 250: 엔진의 본문 오류 (Edge · Android). 읽기 끝 (진행률) 만으로는 Android 에서 덮개가 뜨지 않았다 (931, 노트 10+)
|
||||
onMainFrameError: (u, e) {
|
||||
if (!mounted) return;
|
||||
final host = Uri.tryParse(u)?.host ?? '';
|
||||
if (NetGuard.instance.forcing || NetGuard.instance.failure.value != null) {
|
||||
widget.c.note(trf('웹 페이지를 열지 못함 ({0}): {1}', [host, e]));
|
||||
}
|
||||
_checkDnsBlocked(host: host, error: e);
|
||||
},
|
||||
);
|
||||
|
||||
BookmarksController get bm => widget.bookmarks;
|
||||
|
|
@ -1080,6 +1103,10 @@ class _EdgeViewState extends State<_EdgeView> with WebProxyVisibility {
|
|||
},
|
||||
onTitleChanged: (c, t) => h.onTitle(t ?? ''),
|
||||
onProgressChanged: (c, p) => h.onProgress(p / 100),
|
||||
// 250: 본문을 읽지 못함 (DNS 강제 프록시가 이름을 찾지 못하면 net::ERR_TUNNEL_CONNECTION_FAILED)
|
||||
onReceivedError: (c, req, err) {
|
||||
if (req.isForMainFrame ?? true) h.onMainFrameError?.call(req.url.toString(), err.description);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -79,6 +79,25 @@ class _Nav implements WebNav {
|
|||
Future<void> setJavaScript(bool on) async => javaScript.add(on);
|
||||
}
|
||||
|
||||
/// 웹 뷰 대신: 몇 번 새로 만들어졌는지 센다 (250 - 덮개를 띄워도 웹 뷰를 다시 만들지 않아야 함)
|
||||
class _Probe extends StatefulWidget {
|
||||
static int created = 0;
|
||||
const _Probe();
|
||||
@override
|
||||
State<_Probe> createState() => _ProbeState();
|
||||
}
|
||||
|
||||
class _ProbeState extends State<_Probe> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_Probe.created++;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => const ColoredBox(color: Colors.white);
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('즐겨찾기 트리', () {
|
||||
test('추가 · 찾기 · 이동 · 삭제', () {
|
||||
|
|
@ -325,7 +344,7 @@ void main() {
|
|||
viewBuilder: (h, url) {
|
||||
host = h;
|
||||
h.attach(nav);
|
||||
return const ColoredBox(color: Colors.white);
|
||||
return const _Probe();
|
||||
},
|
||||
),
|
||||
));
|
||||
|
|
@ -335,14 +354,18 @@ void main() {
|
|||
host.onProgress(0.1);
|
||||
host.onUrl('https://blocked.example/');
|
||||
g.reportError('blocked.example', const DnsException('timeout', unreachable: true, server: '192.0.2.1'));
|
||||
_Probe.created = 0;
|
||||
host.onProgress(1);
|
||||
await tester.pump();
|
||||
expect(find.text(msg), findsOneWidget);
|
||||
// 덮개를 띄워도 웹 뷰는 그대로 (새로 만들면 페이지를 다시 읽어 덮개가 걷혔다 - 931)
|
||||
expect(_Probe.created, 0);
|
||||
// [다시 시도]: 덮개를 걷고 다시 읽기
|
||||
await tester.tap(find.text('다시 시도'));
|
||||
await tester.pump();
|
||||
expect(find.text(msg), findsNothing);
|
||||
expect(nav.reloads, 1);
|
||||
expect(_Probe.created, 0);
|
||||
// 다른 이름은 막지 않는다 · 서버가 다시 답한 뒤에는 지난 실패로 막지 않는다
|
||||
host.onProgress(0.1);
|
||||
host.onUrl('https://ok.example/');
|
||||
|
|
@ -366,6 +389,24 @@ void main() {
|
|||
expect(c.settings.dnsMode, 'system');
|
||||
expect(find.text(msg), findsNothing);
|
||||
expect(nav.reloads, 2);
|
||||
|
||||
// 931 (노트 10+): 엔진이 본문 오류 (ERR_TUNNEL_CONNECTION_FAILED) 를 알림 - 그 이름의 실패 기록이 없어도 DNS 가 닿지 않는 중이면 덮개.
|
||||
// 오류 뒤에 오는 읽기 끝이 덮개를 지우지 않는다
|
||||
g.failure.value = DnsFailure('192.0.2.1', 'x', DateTime.now());
|
||||
host.onProgress(0.1);
|
||||
host.onUrl('https://example.org/');
|
||||
host.onMainFrameError!('https://example.org/', 'net::ERR_TUNNEL_CONNECTION_FAILED');
|
||||
host.onProgress(1);
|
||||
await tester.pump();
|
||||
expect(find.text('DNS 서버 (192.0.2.1) 에 닿지 않아 example.org 을(를) 열 수 없습니다.'), findsOneWidget);
|
||||
// DNS 와 상관없는 오류 (이름 기록도 없음) 는 엔진 화면 그대로
|
||||
await tester.tap(find.text('다시 시도'));
|
||||
await tester.pump();
|
||||
host.onProgress(0.1);
|
||||
host.onMainFrameError!('https://example.org/', 'net::ERR_CONNECTION_REFUSED');
|
||||
host.onProgress(1);
|
||||
await tester.pump();
|
||||
expect(find.textContaining('에 닿지 않아'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Android 뒤로 키: 앞 웹 페이지가 있으면 웹 뒤로, 없으면 화면 닫기 · 위쪽 ← 는 늘 닫기', (tester) async {
|
||||
|
|
|
|||
Loading…
Reference in New Issue