CH-J SM Mobilequản lý máy chủ qua SSH
Menu
Mã nguồn đã công bố

CH-J SM Mobile

Duyệt thư mục và tệp của một bản phát hành ứng dụng cụ thể.

Tải mã nguồn dạng ZIP
CH-J / SM Mobile 175 tệp
sm-mobile-20261016T185235Z-8e93ecc1b59a Ngày thêm: · Ngày dựng:
adaptive_session_test.dart Ngày thêm: Chưa ghi nhận ngày 7.4 KB · 198 dòng
Mã Tải tệp xuống
1import 'dart:io';
2import 'dart:ui' show DisplayFeature, DisplayFeatureState, DisplayFeatureType;
3import 'package:flutter/material.dart';
4import 'package:flutter_test/flutter_test.dart';
5import 'package:sm_mobile/core/app_controller.dart';
6import 'package:sm_mobile/main.dart';
7import 'package:sm_mobile/models/server_profile.dart';
8import 'package:sm_mobile/ui/files_page.dart';
9import 'package:sm_mobile/ui/terminal_page.dart';
10import 'package:xterm/xterm.dart';
11import '../support.dart';
13/// Real OpenSSH (tool/test_openssh.py): layout changes must keep the same SSH
14/// client, PTY and SFTP state, and the remote PTY must follow the new size.
15void main() {
16 final enabled = Platform.environment['SM_OPENSSH_TEST'] == '1';
18 DisplayFeature fold(Rect r) => DisplayFeature(
19 bounds: r,
20 type: DisplayFeatureType.fold,
21 state: DisplayFeatureState.postureHalfOpened,
22 );
24 // Logical sizes; physical = logical × ratio.
25 final shapes = <(String, Size, double, double, List<DisplayFeature>)>[
26 ('phone portrait', const Size(390, 844), 3, 0, []),
27 ('phone landscape', const Size(844, 390), 3, 0, []),
28 ('phone landscape + keyboard', const Size(844, 390), 3, 170, []),
29 ('phone portrait + keyboard', const Size(390, 844), 3, 336, []),
30 ('small phone', const Size(320, 568), 2, 0, []),
31 ('iPhone Duo outer @3x (assumed)', const Size(466, 678), 3, 0, []),
32 ('iPhone Duo inner @3x (assumed)', const Size(626, 890), 3, 0, []),
33 (
34 'iPhone Duo inner half folded',
35 const Size(626, 890),
36 3,
37 0,
38 [fold(const Rect.fromLTWH(305, 0, 16, 890))],
39 ),
40 ('tablet portrait', const Size(820, 1180), 2, 0, []),
41 ('tablet landscape', const Size(1180, 820), 2, 0, []),
42 ('tablet split 1/3', const Size(375, 820), 2, 0, []),
43 ('desktop-class', const Size(2560, 1440), 1, 0, []),
44 ('minimum canvas fallback', const Size(200, 260), 2, 0, []),
45 ('phone portrait again', const Size(390, 844), 3, 0, []),
46 ];
48 Future<void> apply(
49 WidgetTester tester,
50 Size s,
51 double ratio,
52 double keyboard,
53 List<DisplayFeature> features,
54 ) async {
55 tester.view.devicePixelRatio = ratio;
56 tester.view.physicalSize = s * ratio;
57 tester.view.viewInsets = FakeViewPadding(bottom: keyboard * ratio);
58 tester.view.displayFeatures = features;
59 await tester.pump();
60 await tester.pump();
61 }
63 Future<void> waitFor(
64 WidgetTester tester,
65 bool Function() done,
66 String what,
67 ) async {
68 final deadline = DateTime.now().add(const Duration(seconds: 15));
69 while (!done()) {
70 if (DateTime.now().isAfter(deadline)) fail('timeout: $what');
71 await tester.runAsync(
72 () => Future<void>.delayed(const Duration(milliseconds: 50)),
73 );
74 await tester.pump();
75 }
76 }
78 testWidgets(
79 'real OpenSSH session, PTY size and SFTP directory survive every layout',
80 (tester) async {
81 addTearDown(tester.view.reset);
82 final c = AppController(MemoryStore());
83 final key = await tester.runAsync(
84 () => File(Platform.environment['SM_TEST_KEY']!).readAsString(),
85 );
86 final root = await tester.runAsync(
87 () => Directory(
88 Platform.environment['SM_TEST_REMOTE_DIR']!,
89 ).resolveSymbolicLinks(),
90 );
91 final dir = '$root/adaptive-${newId()}';
92 await tester.runAsync(() async {
93 await Directory(dir).create(recursive: true);
94 await c.initialize();
95 await c.acceptLicense();
96 await c.openVault('isolated adaptive password', create: true);
97 await c.ssh.connect(
98 ServerProfile(
99 name: 'Isolated OpenSSH',
100 host: '127.0.0.1',
101 username: Platform.environment['USER']!,
102 port: 22229,
103 auth: AuthMethod.privateKey,
104 ),
105 credential: key!,
106 passphrase: 'isolated-test-passphrase',
107 prompt: (_, _, _) async => true,
108 );
109 });
110 expect(c.ssh.error, null);
111 expect(c.ssh.connected, true);
112 final client = c.ssh.client, terminal = c.ssh.terminal;
113 await apply(tester, const Size(390, 844), 3, 0, const []);
114 await tester.pumpWidget(SmMobileApp(c));
115 await tester.pump();
116 await tester.tap(find.byIcon(Icons.terminal).first);
117 await tester.pump();
118 final page = tester.state(find.byType(TerminalPage));
120 var n = 0;
121 for (final (name, size, ratio, keyboard, features) in shapes) {
122 await apply(tester, size, ratio, keyboard, features);
123 expect(tester.takeException(), null, reason: name);
124 expect(c.ssh.connected, true, reason: name);
125 expect(c.ssh.client, same(client), reason: '$name: no reconnect');
126 expect(c.ssh.terminal, same(terminal), reason: name);
127 expect(
128 tester.state(find.byType(TerminalPage)),
129 same(page),
130 reason: name,
131 );
132 // Ask the server for the PTY size it now has.
133 final rows = terminal.viewHeight, cols = terminal.viewWidth;
134 final step = n++;
135 // The echoed command contains only the format string; the expanded
136 // marker can come from the server's answer alone.
137 terminal.onOutput!(
138 "printf 'SZ%s_%s_\\n' $step \"\$(stty size | tr ' ' x)\"\r",
139 );
140 final expected = 'SZ${step}_${rows}x${cols}_';
141 await waitFor(
142 tester,
143 () => terminal.buffer.getText().contains(expected),
144 '$name: remote PTY ${rows}x$cols',
145 );
146 }
148 // Long output and UTF-8: scrollback keeps it, the view follows the
149 // newest line, and painting is clipped to the terminal area.
150 await apply(tester, const Size(390, 844), 3, 0, const []);
151 terminal.onOutput!(
152 "seq 1 3000 | sed 's/^/řádek-/'; printf 'Žluťoučký kůň %s\\n' KONEC\r",
153 );
154 await waitFor(
155 tester,
156 () => terminal.buffer.getText().contains('Žluťoučký kůň KONEC'),
157 'long UTF-8 output',
158 );
159 expect(terminal.buffer.getText(), contains('řádek-3000'));
160 expect(terminal.buffer.lines.length, greaterThan(1000));
161 expect(
162 find.ancestor(
163 of: find.byType(TerminalView),
164 matching: find.byType(ClipRect),
165 ),
166 findsWidgets,
167 );
168 expect(tester.takeException(), null);
170 // SFTP: open a directory, then move through the layouts again.
171 await apply(tester, const Size(390, 844), 3, 0, const []);
172 await tester.tap(find.byIcon(Icons.folder_outlined).first);
173 await tester.pump();
174 await waitFor(
175 tester,
176 () => find.byType(LinearProgressIndicator).evaluate().isEmpty,
177 'SFTP home listing',
178 );
179 final files = tester.state(find.byType(FilesPage)) as dynamic;
180 // The SFTP client lives in the test's fake zone, so its replies are
181 // processed by pumping (waitFor), not inside runAsync.
182 files.load(directory: dir);
183 await waitFor(tester, () => find.text(dir).evaluate().isNotEmpty, dir);
184 for (final (name, size, ratio, keyboard, features) in shapes) {
185 await apply(tester, size, ratio, keyboard, features);
186 expect(tester.takeException(), null, reason: name);
187 expect(c.ssh.client, same(client), reason: '$name: no reconnect');
188 expect(tester.state(find.byType(FilesPage)), same(files), reason: name);
189 expect(find.text(dir), findsOneWidget, reason: '$name: SFTP path');
190 }
192 await tester.pumpWidget(const SizedBox());
193 c.dispose();
194 await tester.runAsync(() => Directory(dir).delete(recursive: true));
195 },
196 skip: !enabled,
197 );
SHA-2565290c8d7f937db62ed578192e42836377a43668a05dff7aa5e2e4c352813fdcb

CH-J Proprietary Software License 1.14 · Mã nguồn được cung cấp theo CH-J Proprietary Software License 1.14. Việc công khai mã nguồn không thay đổi các điều khoản giấy phép và không cấp thêm quyền nào.

SHA-256 của tệp nén46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655
Tệp