캡처 시험: 264 · 265 이동 뒤 (원본에 남음 알림 · 원본 다시 읽기) · SFTP 키 로그인 글 5가지 (관리자 지시 10/21)
- windows_move_left_shot_test: 폴더를 합쳐 옮긴 뒤 알림 "n개는 원본에 남음 (건너뜀)" · 펼쳐 둔 원본 D 를 다시 읽어 남은 것만, ko · en · ja × 960 · 1400. 정식 판 후보rc21b31609로 6개 통과 (캡처 release\test\shots_v20261020_001 moveleft_*) - windows_sshkey_shot_test: [개인 키 파일 고르기] 를 가짜 고르기로, 실제 시험 서버 (paramiko) · ssh-keygen 키로 .ppk 안내 · 고른 뒤 안내 · 암호 필요 · 암호 틀림 · 서버 거부를 ko · en · ja × 412 로 (9f6c5c0의 글, after_1020 으로 3개 통과, 캡처 release\test\shots_f1 sshkey_*). [저장] 은 누르지 않음 (설정 메모리만) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
parent
9f6c5c0bd5
commit
51f16d5548
|
|
@ -0,0 +1,96 @@
|
|||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.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/i18n_controller.dart';
|
||||
import 'package:jj_mkvmaker/l10n/tr.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/explorer_page.dart';
|
||||
import 'package:jj_mkvmaker/ui/theme.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'support/temp_dir.dart';
|
||||
|
||||
/// 264 · 265 (관리자 지시 10/21): 폴더를 합쳐 옮긴 뒤 - 알림에 "n개는 원본에 남음 (건너뜀)" · 펼쳐 둔 원본 폴더를 다시 읽어
|
||||
/// 옮겨진 파일은 보이지 않고 남은 것만 보인다. ko · en · ja × 960 · 1400 (두 창). 시험 폴더 안에서만 (실제 이동, 휴지통 쓰지 않음)
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
final out = Platform.environment['JJ_SHOT_DIR'] ?? '';
|
||||
|
||||
for (final lang in ['ko', 'en', 'ja']) {
|
||||
for (final (w, h) in [(960, 600), (1400, 800)]) {
|
||||
testWidgets('264 · 265 이동 뒤 $lang ${w}x$h', (t) async {
|
||||
t.view.physicalSize = Size(w.toDouble(), h.toDouble());
|
||||
t.view.devicePixelRatio = 1;
|
||||
addTearDown(t.view.reset);
|
||||
await t.runAsync(() => i18n.apply(lang, save: false));
|
||||
addTearDown(() => i18n.apply('ko', save: false));
|
||||
final tmp = testTempDir('jj_moveleft_');
|
||||
final left = p.join(tmp.path, 'left'), right = p.join(tmp.path, 'right');
|
||||
final d = p.join(left, 'D');
|
||||
File(p.join(d, 'same.txt'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('old')
|
||||
..setLastModifiedSync(DateTime(2026, 1, 1));
|
||||
File(p.join(d, 'add.txt')).writeAsStringSync('a');
|
||||
File(p.join(right, 'D', 'same.txt'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('NEWER');
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorerPaths = [left, right]
|
||||
..copyConflict = 'overwrite';
|
||||
final root = p.rootPrefix(tmp.path);
|
||||
ExplorerPage.debugVolumes = () => [(root, root.replaceAll(RegExp(r'[\\/]+$'), ''))];
|
||||
addTearDown(() => ExplorerPage.debugVolumes = null);
|
||||
final shotKey = GlobalKey();
|
||||
await t.pumpWidget(RepaintBoundary(
|
||||
key: shotKey,
|
||||
child: MaterialApp(theme: buildTheme(), debugShowCheckedModeBanner: false, home: ExplorerPage(c: c)),
|
||||
));
|
||||
Future<void> settle(bool Function() done, {int rounds = 150}) async {
|
||||
for (var i = 0; i < rounds && !done(); i++) {
|
||||
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 40)));
|
||||
await t.pump();
|
||||
}
|
||||
await t.pump(const Duration(milliseconds: 300));
|
||||
}
|
||||
|
||||
Future<void> shot(String name) async {
|
||||
expect(t.takeException(), isNull, reason: '$name 넘침');
|
||||
if (out.isEmpty) return;
|
||||
final b = shotKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
|
||||
final img = await t.runAsync(() => b.toImage());
|
||||
final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png));
|
||||
Directory(out).createSync(recursive: true);
|
||||
File(p.join(out, 'moveleft_${name}_${lang}_$w.png')).writeAsBytesSync(bytes!.buffer.asUint8List());
|
||||
}
|
||||
|
||||
await settle(() => find.text('D').evaluate().length >= 2);
|
||||
// 왼쪽 D 를 눌러 펼치고 (고른 것) 안의 add.txt 가 보이게
|
||||
await t.tap(find.text('D').first);
|
||||
await settle(() => find.text('add.txt').evaluate().isNotEmpty);
|
||||
await shot('1_before');
|
||||
await t.tap(find.text(tr('이동')).first);
|
||||
await settle(() => find.widgetWithText(FilledButton, tr('이동')).evaluate().isNotEmpty);
|
||||
await t.tap(find.widgetWithText(FilledButton, tr('이동')));
|
||||
await settle(() => find.byType(LinearProgressIndicator).evaluate().isEmpty && find.byType(SnackBar).evaluate().isNotEmpty);
|
||||
await settle(() => false, rounds: 10); // 알림이 다 올라오게
|
||||
expect(File(p.join(right, 'D', 'add.txt')).existsSync(), isTrue);
|
||||
expect(File(p.join(d, 'same.txt')).readAsStringSync(), 'old', reason: '받는 쪽이 더 새것이라 원본에 남음');
|
||||
expect(find.textContaining(trf('{0}개는 원본에 남음 (건너뜀)', [1])), findsOneWidget, reason: '264: 알림에');
|
||||
expect(find.text('add.txt'), findsNothing, reason: '265: 원본 D 를 다시 읽음');
|
||||
expect(find.text('same.txt'), findsOneWidget);
|
||||
await shot('2_after');
|
||||
ScaffoldMessenger.of(t.element(find.byType(ExplorerPage))).clearSnackBars();
|
||||
await settle(() => false, rounds: 20);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.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/i18n_controller.dart';
|
||||
import 'package:jj_mkvmaker/core/sftp.dart';
|
||||
import 'package:jj_mkvmaker/l10n/tr.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/sftp_settings.dart';
|
||||
import 'package:jj_mkvmaker/ui/theme.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../test/support/sftp_server.dart';
|
||||
import 'support/temp_dir.dart';
|
||||
|
||||
/// [개인 키 파일 고르기] 가 고르는 파일을 시험이 정한다 (실제 파일 창은 열지 않음)
|
||||
class _PickStorage extends DesktopStorageService {
|
||||
String? next;
|
||||
@override
|
||||
Future<List<String>> pickFiles({required String title, required List<String> extensions, String? initialDirectory}) async =>
|
||||
next == null ? const [] : [next!];
|
||||
}
|
||||
|
||||
/// 관리자 지시 (10/21): SFTP 키 로그인의 사용자 글 - 키 고른 뒤 안내 · 암호 필요 · 암호 틀림 · 서버 거부 · .ppk 안내 를
|
||||
/// ko · en · ja × 412 로 찍는다. 실제 시험 서버 (paramiko) · ssh-keygen 으로 만든 시험 키만 쓴다 (시험 폴더 안, 설정은 메모리만)
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
final out = Platform.environment['JJ_SHOT_DIR'] ?? '';
|
||||
final keygen = r'C:\Windows\System32\OpenSSH\ssh-keygen.exe';
|
||||
final skip = !sftpTestServerAvailable || !File(keygen).existsSync();
|
||||
|
||||
for (final lang in ['ko', 'en', 'ja']) {
|
||||
testWidgets('SSH 키 글 $lang 412x900', (t) async {
|
||||
t.view.physicalSize = const Size(412, 900);
|
||||
t.view.devicePixelRatio = 1;
|
||||
addTearDown(t.view.reset);
|
||||
await t.runAsync(() => i18n.apply(lang, save: false));
|
||||
addTearDown(() => i18n.apply('ko', save: false));
|
||||
final tmp = testTempDir('jj_sshkeyshot_');
|
||||
final root = Directory(p.join(tmp.path, 'root'))..createSync();
|
||||
Future<String> gen(String name, String pass) async {
|
||||
final f = p.join(tmp.path, name);
|
||||
final r = await Process.run(keygen, ['-q', '-t', 'ed25519', '-N', pass, '-f', f, '-C', 'jj-test']);
|
||||
expect(r.exitCode, 0, reason: '${r.stderr}');
|
||||
return f;
|
||||
}
|
||||
|
||||
late SftpTestServer server;
|
||||
late String keyA, keyB;
|
||||
await t.runAsync(() async {
|
||||
keyA = await gen('a', 'kp');
|
||||
keyB = await gen('b', '');
|
||||
server = await SftpTestServer.start(root: root.path, hostKeyFile: p.join(tmp.path, 'hostkey'), password: null, pubkey: '$keyA.pub');
|
||||
});
|
||||
addTearDown(() => t.runAsync(server.stop));
|
||||
final ppk = File(p.join(tmp.path, 'key.ppk'))
|
||||
..writeAsStringSync('PuTTY-User-Key-File-3: ssh-ed25519\nEncryption: none\nComment: x\nPublic-Lines: 1\nAAAA\n');
|
||||
final storage = _PickStorage();
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: storage));
|
||||
final old = SftpServer(id: 's1', name: 'NAS', host: '127.0.0.1', port: server.port, user: 'jj', hostKeyFingerprint: server.fingerprint);
|
||||
final navKey = GlobalKey<NavigatorState>();
|
||||
final shotKey = GlobalKey();
|
||||
await t.pumpWidget(RepaintBoundary(
|
||||
key: shotKey,
|
||||
child: MaterialApp(
|
||||
theme: buildTheme(), debugShowCheckedModeBanner: false, navigatorKey: navKey, home: const Scaffold(body: SizedBox.expand())),
|
||||
));
|
||||
Future<void> settle(bool Function() done, {int rounds = 150}) async {
|
||||
for (var i = 0; i < rounds && !done(); i++) {
|
||||
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 40)));
|
||||
await t.pump();
|
||||
}
|
||||
await t.pump(const Duration(milliseconds: 200));
|
||||
}
|
||||
|
||||
Future<void> shot(String name, String expectText) async {
|
||||
final f = find.textContaining(expectText);
|
||||
await settle(() => f.evaluate().isNotEmpty);
|
||||
expect(f, findsOneWidget, reason: name);
|
||||
await t.ensureVisible(f);
|
||||
await settle(() => false, rounds: 5);
|
||||
expect(t.takeException(), isNull, reason: '$name 넘침');
|
||||
if (out.isEmpty) return;
|
||||
final b = shotKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
|
||||
final img = await t.runAsync(() => b.toImage());
|
||||
final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png));
|
||||
Directory(out).createSync(recursive: true);
|
||||
File(p.join(out, 'sshkey_${name}_${lang}_412.png')).writeAsBytesSync(bytes!.buffer.asUint8List());
|
||||
}
|
||||
|
||||
Future<void> pick(String file) async {
|
||||
storage.next = file;
|
||||
final b = find.byIcon(Icons.key_outlined);
|
||||
await t.ensureVisible(b);
|
||||
await t.tap(b);
|
||||
}
|
||||
|
||||
Future<void> testConnection() async {
|
||||
final b = find.text(tr('연결 확인'));
|
||||
await t.ensureVisible(b);
|
||||
await t.tap(b);
|
||||
await settle(() => find.byType(LinearProgressIndicator).evaluate().isNotEmpty, rounds: 20);
|
||||
await settle(() => find.byType(LinearProgressIndicator).evaluate().isEmpty, rounds: 400);
|
||||
}
|
||||
|
||||
unawaited(editSftpServer(navKey.currentContext!, c, old: old));
|
||||
await settle(() => find.text(tr('연결 확인')).evaluate().isNotEmpty);
|
||||
// ① PuTTY 키를 고르면 바꾸는 법
|
||||
await pick(ppk.path);
|
||||
await shot('1_ppk', 'Export OpenSSH key');
|
||||
// ② 암호가 걸린 키를 고르면 "아래에 키 암호를"
|
||||
await pick(keyA);
|
||||
await shot('2_picked', tr('개인 키를 넣었습니다. 암호가 걸린 키라 아래에 키 암호를 넣으세요 (원래 파일 경로는 저장하지 않습니다)'));
|
||||
// ③ 키 암호 없이 [연결 확인]
|
||||
await testConnection();
|
||||
await shot('3_needpass', tr('개인 키에 암호가 걸려 있습니다. 키 암호를 넣으세요'));
|
||||
// ④ 틀린 키 암호
|
||||
await t.enterText(find.widgetWithText(TextField, tr('개인 키 암호')), 'nope');
|
||||
await testConnection();
|
||||
await shot('4_badpass', tr('개인 키의 암호가 맞지 않습니다'));
|
||||
// ⑤ 서버가 모르는 키 (짝 공개 키가 서버에 없음)
|
||||
await pick(keyB);
|
||||
await settle(() => false, rounds: 5);
|
||||
await testConnection();
|
||||
await shot('5_rejected', 'authorized_keys');
|
||||
navKey.currentState!.popUntil((r) => r.isFirst);
|
||||
await settle(() => false, rounds: 10);
|
||||
expect(c.settings.sftpServers, isEmpty, reason: '[저장] 은 누르지 않음');
|
||||
}, skip: skip, timeout: const Timeout(Duration(minutes: 3)));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue