CH-J SM Mobilepengurusan pelayan melalui SSH
Menu
Sumber yang diterbitkan

CH-J SM Mobile

Layari direktori dan fail untuk keluaran aplikasi tertentu.

Muat turun ZIP sumber
CH-J / SM Mobile 175 fail
sm-mobile-20261016T185235Z-8e93ecc1b59a Ditambah: · Dibina:
editor_page_test.dart Ditambah: Tarikh tidak direkodkan 10.3 KB · 303 baris
Kod Muat turun fail
1import 'dart:convert';
2import 'dart:typed_data';
3import 'package:flutter/material.dart';
4import 'package:flutter_test/flutter_test.dart';
5import 'package:re_editor/re_editor.dart';
6import 'package:sm_mobile/core/app_controller.dart';
7import 'package:sm_mobile/core/failure.dart';
8import 'package:sm_mobile/l10n/strings.dart';
9import 'package:sm_mobile/services/editor/sources.dart';
10import 'package:sm_mobile/services/editor/text_codec.dart';
11import 'package:sm_mobile/ui/editor/editor_page.dart';
12import '../support.dart';
14/// A file kept in memory, with the same conflict rule as real sources.
15class MemorySource implements FileSource {
16 @override
17 final String name;
18 Uint8List bytes;
19 int version = 0, loadedVersion = 0, saves = 0;
20 MemorySource(this.name, this.bytes);
21 @override
22 String get location => 'memory:$name';
23 @override
24 String get kind => 'device';
25 @override
26 String get key => location;
27 @override
28 bool get canSave => true;
29 @override
30 String? get readOnlyReason => null;
31 @override
32 Future<Uint8List> load({int maxBytes = maxEditableBytes}) async {
33 if (bytes.length > maxBytes) throw const Failure('fileTooLarge');
34 loadedVersion = version;
35 return bytes;
36 }
38 @override
39 Future<void> save(
40 Uint8List next, {
41 bool force = false,
42 bool inPlace = false,
43 }) async {
44 if (!force && version != loadedVersion) {
45 throw const Failure('documentChanged');
46 }
47 bytes = next;
48 version++;
49 loadedVersion = version;
50 saves++;
51 }
53 @override
54 Map<String, dynamic> toJson() => {'kind': 'new', 'name': name};
57void main() {
58 late AppController c;
59 setUp(() async {
60 c = AppController(MemoryStore());
61 });
63 Future<void> open(WidgetTester tester, MemorySource source) async {
64 tester.view.physicalSize = const Size(1000, 1600);
65 tester.view.devicePixelRatio = 1;
66 addTearDown(tester.view.reset);
67 await tester.runAsync(() async {
68 await c.initialize();
69 await c.acceptLicense();
70 if (!c.vault.unlocked) {
71 await c.openVault('editor test password', create: true);
72 }
73 });
74 await tester.pumpWidget(
75 MaterialApp(
76 locale: const Locale('en'),
77 localizationsDelegates: AppLocalizations.localizationsDelegates,
78 supportedLocales: AppLocalizations.supportedLocales,
79 home: EditorPage(c, source),
80 ),
81 );
82 await tester.runAsync(() async {
83 for (var i = 0; i < 3000; i++) {
84 await tester.pump(const Duration(milliseconds: 10));
85 if (find.byType(CodeEditor).evaluate().isNotEmpty ||
86 find.byIcon(Icons.error_outline).evaluate().isNotEmpty ||
87 find.byIcon(Icons.memory).evaluate().isNotEmpty) {
88 break;
89 }
90 await Future<void>.delayed(const Duration(milliseconds: 10));
91 }
92 });
93 await tester.pump();
94 }
96 CodeLineEditingController editor(WidgetTester tester) =>
97 tester.widget<CodeEditor>(find.byType(CodeEditor)).controller!;
99 Future<void> save(WidgetTester tester) async {
100 await tester.tap(find.byIcon(Icons.save));
101 await tester.runAsync(() async {
102 for (var i = 0; i < 300; i++) {
103 await tester.pump(const Duration(milliseconds: 10));
104 if (find.byType(CircularProgressIndicator).evaluate().isEmpty) break;
105 await Future<void>.delayed(const Duration(milliseconds: 10));
106 }
107 });
108 await tester.pump();
109 }
111 Future<void> close(WidgetTester tester) async {
112 // Stop the cursor blink (a real timer in re_editor) and let snack bars
113 // and debounce timers run out.
114 FocusManager.instance.primaryFocus?.unfocus();
115 await tester.runAsync(
116 () => Future<void>.delayed(const Duration(milliseconds: 600)),
117 );
118 await tester.pump(const Duration(seconds: 10));
119 await tester.pumpWidget(const SizedBox());
120 await tester.pump(const Duration(seconds: 10));
121 c.dispose();
122 }
124 final samples = <String, (String, String)>{
125 'notes.txt': ('Hello\nworld\n', 'Text'),
126 'index.html': (
127 '<!DOCTYPE html>\n<html><body><p>Hi</p></body></html>\n',
128 'HTML',
129 ),
130 'index.php': ('<?php\necho "Ahoj";\n', 'PHP'),
131 'data.json': ('{\n "a": 1\n}\n', 'JSON'),
132 'compose.yaml': ('services:\n web:\n image: nginx\n', 'YAML'),
133 'feed.xml': ('<?xml version="1.0"?>\n<a><b/></a>\n', 'XML'),
134 'site.css': ('body {\n color: red;\n}\n', 'CSS'),
135 'app.js': ('const a = 1;\nconsole.log(a);\n', 'JavaScript'),
136 'tool.py': ('def main():\n print("x")\n', 'Python'),
137 'deploy.sh': ('#!/bin/sh\necho ok\n', 'Shell (Bash/sh/zsh)'),
138 'nginx.conf': ('server {\r\n listen 80;\r\n}\r\n', 'NGINX'),
139 'web.service': (
140 '[Unit]\nDescription=Web\n\n[Service]\nExecStart=/bin/true\n',
141 'systemd unit',
142 ),
143 'motd': ('Welcome\nto the server', 'Text'),
144 };
146 for (final MapEntry(key: name, value: (text, syntax)) in samples.entries) {
147 testWidgets('open, edit and save $name', (tester) async {
148 final source = MemorySource(name, Uint8List.fromList(utf8.encode(text)));
149 await open(tester, source);
150 expect(find.text(syntax), findsOneWidget, reason: 'syntax shown');
151 final e = editor(tester);
152 expect(e.text, text.replaceAll('\r\n', '\n').isEmpty ? '' : e.text);
153 // Insert at the start of line 1.
154 e.selection = const CodeLineSelection.collapsed(index: 0, offset: 0);
155 e.replaceSelection('# edit\n');
156 await tester.pump();
157 expect(find.textContaining('● $name'), findsOneWidget, reason: 'dirty');
158 await save(tester);
159 final crlf = text.contains('\r\n');
160 final expected = (crlf ? '# edit\r\n' : '# edit\n') + text;
161 expect(utf8.decode(source.bytes), expected);
162 expect(source.saves, 1);
163 expect(find.textContaining('● '), findsNothing);
164 await close(tester);
165 });
166 }
168 testWidgets('Windows-1250 and UTF-16 stay in their encoding', (tester) async {
169 final source = MemorySource(
170 'legacy.txt',
171 encodeText('Žluťoučký kůň\r\n', TextEncoding.windows1250),
172 );
173 await open(tester, source);
174 // Unknown 8-bit encoding: the user is asked, nothing is guessed.
175 expect(find.text('Unknown text encoding'), findsOneWidget);
176 await tester.tap(find.text('Windows-1250'));
177 await tester.runAsync(() async {
178 for (var i = 0; i < 100; i++) {
179 await tester.pump(const Duration(milliseconds: 10));
180 if (find.byType(CodeEditor).evaluate().isNotEmpty) break;
181 }
182 });
183 await tester.pump();
184 final e = editor(tester);
185 e.selection = CodeLineSelection.collapsed(
186 index: 0,
187 offset: e.codeLines[0].length,
188 );
189 e.replaceSelection('!');
190 await tester.pump();
191 await save(tester);
192 expect(
193 source.bytes,
194 encodeText('Žluťoučký kůň!\r\n', TextEncoding.windows1250),
195 );
196 await close(tester);
198 final wide = MemorySource(
199 'w.txt',
200 encodeText('a\nb\n', TextEncoding.utf16be),
201 );
202 c = AppController(MemoryStore());
203 await open(tester, wide);
204 expect(find.text('UTF-16 BE'), findsOneWidget);
205 final w = editor(tester);
206 w.selection = const CodeLineSelection.collapsed(index: 0, offset: 1);
207 w.replaceSelection('Z');
208 await tester.pump();
209 await save(tester);
210 expect(wide.bytes, encodeText('aZ\nb\n', TextEncoding.utf16be));
211 await close(tester);
212 });
214 testWidgets('a change elsewhere is a conflict, not a silent overwrite', (
215 tester,
216 ) async {
217 final source = MemorySource(
218 'a.txt',
219 Uint8List.fromList(utf8.encode('one\n')),
220 );
221 await open(tester, source);
222 source.version++; // changed by another app
223 final e = editor(tester);
224 e.selection = const CodeLineSelection.collapsed(index: 0, offset: 0);
225 e.replaceSelection('x');
226 await tester.pump();
227 await save(tester);
228 expect(find.text('The file on the device has changed'), findsOneWidget);
229 expect(source.saves, 0);
230 await tester.tap(find.text('Overwrite'));
231 await tester.runAsync(() async {
232 for (var i = 0; i < 100 && source.saves == 0; i++) {
233 await tester.pump(const Duration(milliseconds: 10));
234 await Future<void>.delayed(const Duration(milliseconds: 10));
235 }
236 });
237 expect(utf8.decode(source.bytes), 'xone\n');
238 await close(tester);
239 });
241 testWidgets('binary files are not opened as text without asking', (
242 tester,
243 ) async {
244 final source = MemorySource(
245 'blob',
246 // A PNG header followed by mixed binary data.
247 Uint8List.fromList([
248 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, //
249 for (var i = 0; i < 512; i++) (i * 37) % 256,
250 ]),
251 );
252 await open(tester, source);
253 expect(find.text('This looks like a binary file.'), findsOneWidget);
254 expect(find.byType(CodeEditor), findsNothing);
255 await close(tester);
256 });
258 testWidgets('unsaved text becomes a draft when the Vault locks', (
259 tester,
260 ) async {
261 final source = MemorySource(
262 'd.txt',
263 Uint8List.fromList(utf8.encode('x\n')),
264 );
265 await open(tester, source);
266 final e = editor(tester);
267 e.selection = const CodeLineSelection.collapsed(index: 0, offset: 1);
268 e.replaceSelection('yz');
269 await tester.pump();
270 final draft = c.draftSources.single();
271 expect(draft!.json['text'], 'xyz\n');
272 expect(draft.json['encoding'], 'utf8');
273 await close(tester);
274 });
276 for (final kb in [1, 100, 1024, 5 * 1024, 10 * 1024]) {
277 testWidgets('$kb KB file opens, edits and saves intact', (tester) async {
278 final line = 'line with some text, čeština a € ${'x' * 40}\n';
279 final buffer = StringBuffer();
280 while (buffer.length < kb * 1024) {
281 buffer.write(line);
282 }
283 final bytes = Uint8List.fromList(utf8.encode(buffer.toString()));
284 final source = MemorySource('big.log', bytes);
285 final watch = Stopwatch()..start();
286 await open(tester, source);
287 final opened = watch.elapsedMilliseconds;
288 final e = editor(tester);
289 expect(e.lineCount, greaterThan(kb * 1024 ~/ line.length));
290 e.selection = const CodeLineSelection.collapsed(index: 0, offset: 0);
291 e.replaceSelection('>');
292 await tester.pump();
293 watch.reset();
294 await save(tester);
295 final saved = watch.elapsedMilliseconds;
296 expect(source.bytes.length, bytes.length + 1);
297 expect(source.bytes.sublist(1), bytes);
298 // ignore: avoid_print
299 print('editor $kb KB: open $opened ms, save $saved ms');
300 await close(tester);
301 }, timeout: const Timeout(Duration(minutes: 3)));
302 }
SHA-25668585353e30b5fe9cbb135708893164f12e4d7fecdada8e0d893033bb5c47bf8

CH-J Proprietary Software License 1.14 · Kod sumber disediakan di bawah CH-J Proprietary Software License 1.14. Ketersediaannya tidak mengubah terma lesen atau memberikan hak tambahan.

SHA-256 arkib46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655
Fail