CH-J SM Mobileإدارة الخوادم عبر SSH
القائمة
المصدر المنشور

CH-J SM Mobile

تصفّح الأدلة والملفات الخاصة بإصدار محدد من التطبيق.

تنزيل الشيفرة المصدرية بصيغة ZIP
CH-J / SM Mobile 175 ملفات
sm-mobile-20261016T185235Z-8e93ecc1b59a تاريخ الإضافة: · تاريخ البناء:
adaptive_layout_test.dart تاريخ الإضافة: لم يُسجَّل التاريخ 25.1 KB · 725 أسطر
الشيفرة تنزيل الملف
1import 'dart:math' as math;
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/ui/adaptive.dart';
8import 'package:sm_mobile/ui/diagnostics_page.dart';
9import 'package:sm_mobile/ui/servers_page.dart';
10import 'support.dart';
12// Breakpoints and the tested range. The range is what the tests sweep, not a
13// limit of supported devices.
14const breakpoints = [
15 WindowClass.mediumMin,
16 WindowClass.expandedMin,
17 WindowClass.largeMin,
18 WindowClass.extraLargeMin,
19];
20const minWidth = 240.0,
21 maxWidth = 2560.0,
22 minHeight = 320.0,
23 maxHeight = 3000.0;
25/// Widths: range ends, an even spread, and every breakpoint -1/-0.5/0/+0.5/+1.
26List<double> generatedWidths({int steps = 12}) => <double>{
27 minWidth,
28 maxWidth,
29 for (var i = 0; i <= steps; i++)
30 (minWidth + (maxWidth - minWidth) * i / steps).roundToDouble(),
31 for (final b in breakpoints) ...[b - 1, b - .5, b, b + .5, b + 1],
32}.toList()..sort();
34List<double> generatedHeights({int steps = 8}) => <double>{
35 minHeight,
36 maxHeight,
37 479,
38 480,
39 481,
40 for (var i = 0; i <= steps; i++)
41 (minHeight + (maxHeight - minHeight) * i / steps).roundToDouble(),
42}.toList()..sort();
44const ratios = [1.0, 1.5, 2.0, 2.625, 3.0, 3.5];
45const textScales = [1.0, 1.3, 2.0];
47/// Safe areas in logical pixels: none, portrait notch, landscape notch,
48/// and a status area beside the content.
49const paddings = [
50 EdgeInsets.zero,
51 EdgeInsets.only(top: 59, bottom: 34),
52 EdgeInsets.only(left: 59, right: 59, bottom: 21),
53 EdgeInsets.only(right: 44),
54];
56class Shape {
57 final Size size;
58 final double ratio, text;
59 final EdgeInsets padding;
60 final EdgeInsets keyboard;
61 final List<DisplayFeature> features;
62 const Shape(
63 this.size, {
64 this.ratio = 3,
65 this.text = 1,
66 this.padding = EdgeInsets.zero,
67 this.keyboard = EdgeInsets.zero,
68 this.features = const [],
69 });
70 @override
71 String toString() =>
72 '${size.width}x${size.height}dp @${ratio}x text${text}x '
73 'pad$padding kb${keyboard.bottom} features${features.length}';
76/// Every width × height combination, cycling ratio, text size and safe area
77/// deterministically so that each value meets many window shapes.
78List<Shape> generatedShapes() {
79 final out = <Shape>[];
80 var n = 0;
81 for (final w in generatedWidths()) {
82 for (final h in generatedHeights()) {
83 out.add(
84 Shape(
85 Size(w, h),
86 ratio: ratios[n % ratios.length],
87 text: textScales[(n ~/ 2) % textScales.length],
88 padding: paddings[(n ~/ 3) % paddings.length],
89 ),
90 );
91 n++;
92 }
93 }
94 return out;
97FakeViewPadding fake(EdgeInsets e, double ratio) => FakeViewPadding(
98 left: e.left * ratio,
99 top: e.top * ratio,
100 right: e.right * ratio,
101 bottom: e.bottom * ratio,
102);
104Future<void> apply(WidgetTester tester, Shape s) async {
105 final v = tester.view;
106 v.devicePixelRatio = s.ratio;
107 // Physical pixels = logical size × devicePixelRatio. Layout only ever sees
108 // the logical size.
109 v.physicalSize = Size(s.size.width * s.ratio, s.size.height * s.ratio);
110 v.padding = fake(s.padding, s.ratio);
111 v.viewPadding = fake(s.padding, s.ratio);
112 v.viewInsets = fake(s.keyboard, s.ratio);
113 v.displayFeatures = s.features;
114 tester.platformDispatcher.textScaleFactorTestValue = s.text;
115 await tester.pump();
116 await tester.pump(const Duration(milliseconds: 50));
119void reset(WidgetTester tester) {
120 tester.view.reset();
121 tester.platformDispatcher.clearTextScaleFactorTestValue();
124Future<AppController> openWorkspace(WidgetTester tester) async {
125 final c = AppController(MemoryStore());
126 await tester.runAsync(() async {
127 await c.initialize();
128 await c.acceptLicense();
129 await c.openVault('adaptive layout test password', create: true);
130 });
131 await tester.pumpWidget(SmMobileApp(c));
132 await tester.pumpAndSettle();
133 return c;
136Future<void> closeWorkspace(WidgetTester tester, AppController c) async {
137 await tester.pumpWidget(const SizedBox());
138 c.dispose();
141/// Opens a page through whatever navigation the current layout shows.
142Future<void> go(WidgetTester tester, IconData icon) async {
143 final nav = find.byWidgetPredicate(
144 (w) => w is NavigationRail || w is NavigationBar,
145 );
146 final direct = find.descendant(of: nav, matching: find.byIcon(icon));
147 if (direct.evaluate().isNotEmpty) {
148 await tester.tap(direct.first);
149 } else if (icon == Icons.dns_outlined &&
150 find.byType(NavigationRail).evaluate().isNotEmpty) {
151 // Two panes: the server list is always on screen.
152 expect(find.byType(ServersPage), findsOneWidget);
153 return;
154 } else {
155 // The fourth bottom bar item opens the remaining destinations.
156 await tester.tap(find.byType(NavigationDestination).last);
157 await tester.pumpAndSettle();
158 await tester.tap(
159 find.descendant(of: find.byType(ListTile), matching: find.byIcon(icon)),
160 );
161 }
162 await tester.pumpAndSettle();
165const pageIcons = [
166 Icons.dns_outlined,
167 Icons.terminal,
168 Icons.folder_outlined,
169 Icons.monitor_heart_outlined,
170 Icons.network_check,
171 Icons.settings_outlined,
172];
174DisplayFeature crease(
175 Rect bounds, {
176 DisplayFeatureType type = DisplayFeatureType.fold,
177 DisplayFeatureState state = DisplayFeatureState.postureHalfOpened,
178}) => DisplayFeature(bounds: bounds, type: type, state: state);
180void main() {
181 group('window classes', () {
182 test('every breakpoint just below, on and just above', () {
183 expect(WindowClass.ofWidth(0), WindowClass.compact);
184 final classes = WindowClass.values;
185 for (var i = 0; i < breakpoints.length; i++) {
186 final b = breakpoints[i];
187 for (final d in [1.0, .5, .01]) {
188 expect(WindowClass.ofWidth(b - d), classes[i], reason: '${b - d}');
189 expect(
190 WindowClass.ofWidth(b + d),
191 classes[i + 1],
192 reason: '${b + d}',
193 );
194 }
195 expect(WindowClass.ofWidth(b), classes[i + 1], reason: '$b');
196 }
197 expect(WindowClass.ofWidth(100000), WindowClass.extraLarge);
198 });
200 test('class grows monotonically over the generated range', () {
201 var last = WindowClass.compact;
202 for (var w = 1.0; w <= 4000; w += 0.5) {
203 final c = WindowClass.ofWidth(w);
204 expect(c.index >= last.index, true, reason: '$w');
205 last = c;
206 }
207 });
209 test('class ignores devicePixelRatio: only logical size counts', () {
210 for (final w in generatedWidths()) {
211 final expected = WindowClass.ofWidth(w);
212 for (final r in ratios) {
213 // A physical width divided by its ratio is the same logical width.
214 expect(WindowClass.ofWidth(w * r / r), expected);
215 }
216 }
217 });
219 test('navigation and pane rules', () {
220 AdaptiveLayout l(
221 double w,
222 double h, {
223 EdgeInsets? pad,
224 List<DisplayFeature>? f,
225 }) => AdaptiveLayout(
226 Size(w, h),
227 padding: pad ?? EdgeInsets.zero,
228 features: f ?? const [],
229 );
230 expect(l(390, 844).useRail, false);
231 expect(l(844, 390).useRail, true, reason: 'short landscape phone');
232 expect(l(479, 320).useRail, false, reason: 'too narrow for a rail');
233 expect(l(568, 320).useRail, true);
234 expect(l(599, 900).useRail, false);
235 expect(l(600, 900).useRail, true);
236 expect(l(500, 900, pad: const EdgeInsets.only(right: 44)).useRail, true);
237 expect(l(839, 1000).twoPane, false);
238 expect(l(840, 1000).twoPane, true);
239 // A flat fold does not separate; a half-opened fold or a hinge does.
240 final flat = crease(
241 const Rect.fromLTWH(420, 0, 0, 900),
242 state: DisplayFeatureState.postureFlat,
243 );
244 expect(l(840, 900, f: [flat]).separatingFeature, null);
245 final half = crease(const Rect.fromLTWH(300, 0, 0, 900));
246 expect(l(600, 900, f: [half]).twoPane, true);
247 final hinge = crease(
248 const Rect.fromLTWH(290, 0, 20, 900),
249 type: DisplayFeatureType.hinge,
250 state: DisplayFeatureState.unknown,
251 );
252 expect(l(600, 900, f: [hinge]).separatingFeature, hinge);
253 final cutout = crease(
254 const Rect.fromLTWH(150, 0, 100, 30),
255 type: DisplayFeatureType.cutout,
256 state: DisplayFeatureState.unknown,
257 );
258 expect(l(400, 900, f: [cutout]).separatingFeature, null);
259 final table = crease(const Rect.fromLTWH(0, 450, 840, 0));
260 expect(l(840, 900, f: [table]).horizontalFold, true);
261 expect(l(840, 900, f: [table]).useRail, false);
262 });
263 });
265 testWidgets(
266 'generated sizes 240–2560 × 320–3000 dp on every page, resized live',
267 (tester) async {
268 addTearDown(() => reset(tester));
269 await apply(tester, const Shape(Size(700, 1000)));
270 final c = await openWorkspace(tester);
271 final shapes = generatedShapes();
272 final failures = <String>[];
273 var checked = 0;
274 for (var page = 0; page < pageIcons.length; page++) {
275 await apply(tester, const Shape(Size(700, 1000)));
276 await go(tester, pageIcons[page]);
277 // Each page meets the shapes in a different order, so it is resized
278 // from many different previous shapes.
279 final order = [...shapes]..shuffle(math.Random(page));
280 for (final s in order) {
281 await apply(tester, s);
282 checked++;
283 final error = tester.takeException();
284 if (error != null) failures.add('page $page $s: $error');
285 final layout = AdaptiveLayout(s.size, padding: s.padding);
286 final canvas =
287 s.size.width >= minimumCanvas.width &&
288 s.size.height >= minimumCanvas.height;
289 if (canvas) {
290 final rail = find.byType(NavigationRail).evaluate().length;
291 final bar = find.byType(NavigationBar).evaluate().length;
292 if (layout.useRail != (rail == 1) || layout.useRail == (bar == 1)) {
293 failures.add('page $page $s: navigation rail=$rail bar=$bar');
294 }
295 }
296 }
297 }
298 expect(failures, isEmpty, reason: failures.take(20).join('\n'));
299 expect(checked, greaterThan(1000));
300 await closeWorkspace(tester, c);
301 },
302 );
304 testWidgets('every destination is reachable at representative shapes', (
305 tester,
306 ) async {
307 addTearDown(() => reset(tester));
308 await apply(tester, const Shape(Size(390, 844)));
309 final c = await openWorkspace(tester);
310 // Shapes below the minimum canvas are covered by their own test.
311 for (final size in const [
312 Size(320, 568),
313 Size(360, 640),
314 Size(375, 667),
315 Size(390, 844),
316 Size(430, 932),
317 Size(600, 960),
318 Size(768, 1024),
319 Size(820, 1180),
320 Size(1024, 1366),
321 Size(1280, 800),
322 Size(1440, 900),
323 Size(240, 320),
324 Size(2560, 1600),
325 ]) {
326 for (final s in [
327 size,
328 size.flipped,
329 ].where((s) => s.height >= minimumCanvas.height)) {
330 for (final text in [1.0, 2.0]) {
331 await apply(tester, Shape(s, text: text));
332 for (var page = 1; page < pageIcons.length; page++) {
333 await go(tester, pageIcons[page]);
334 expect(tester.takeException(), null, reason: '$s page $page');
335 }
336 }
337 }
338 }
339 await closeWorkspace(tester, c);
340 });
342 testWidgets('below the minimum canvas the app scrolls instead of breaking', (
343 tester,
344 ) async {
345 addTearDown(() => reset(tester));
346 await apply(tester, const Shape(Size(390, 844)));
347 final c = await openWorkspace(tester);
348 for (final s in const [
349 Size(120, 160),
350 Size(200, 300),
351 Size(239, 319),
352 Size(600, 200),
353 Size(1, 1),
354 ]) {
355 await apply(tester, Shape(s, text: 2));
356 expect(tester.takeException(), null, reason: '$s');
357 // Content is laid out on the minimum canvas and remains scrollable.
358 final scaffold = tester.getSize(find.byType(Scaffold).first);
359 expect(scaffold.width, greaterThanOrEqualTo(minimumCanvas.width));
360 expect(scaffold.height, greaterThanOrEqualTo(minimumCanvas.height));
361 }
362 await apply(tester, const Shape(Size(390, 844)));
363 expect(tester.getSize(find.byType(Scaffold).first), const Size(390, 844));
364 await closeWorkspace(tester, c);
365 });
367 testWidgets('software keyboard over every page and large text', (
368 tester,
369 ) async {
370 addTearDown(() => reset(tester));
371 await apply(tester, const Shape(Size(390, 844)));
372 final c = await openWorkspace(tester);
373 for (var page = 0; page < pageIcons.length; page++) {
374 await apply(tester, const Shape(Size(390, 844)));
375 await go(tester, pageIcons[page]);
376 for (final s in const [
377 Shape(Size(390, 844), keyboard: EdgeInsets.only(bottom: 336)),
378 Shape(Size(320, 568), keyboard: EdgeInsets.only(bottom: 260), text: 2),
379 Shape(Size(844, 390), keyboard: EdgeInsets.only(bottom: 200)),
380 Shape(
381 Size(568, 320),
382 keyboard: EdgeInsets.only(bottom: 200),
383 text: 1.3,
384 ),
385 Shape(Size(1024, 768), keyboard: EdgeInsets.only(bottom: 400)),
386 Shape(Size(768, 1024), keyboard: EdgeInsets.only(bottom: 320), text: 3),
387 ]) {
388 await apply(tester, s);
389 expect(tester.takeException(), null, reason: 'page $page $s');
390 }
391 }
392 await closeWorkspace(tester, c);
393 });
395 testWidgets(
396 'typed data and page state survive every class, rotation and fold',
397 (tester) async {
398 addTearDown(() => reset(tester));
399 await apply(tester, const Shape(Size(390, 844)));
400 final c = await openWorkspace(tester);
401 // Unsaved input: diagnostics form, and the server search once the list
402 // is shown next to it.
403 await go(tester, Icons.network_check);
404 await tester.enterText(
405 find.byType(TextField).first,
406 'draft.example.test',
407 );
408 final diagnostics = tester.state(find.byType(DiagnosticsPage));
409 State? servers;
410 final sequence = <Shape>[
411 for (final w in generatedWidths(steps: 6)) Shape(Size(w, 900)),
412 for (final w in generatedWidths(steps: 6).reversed) Shape(Size(900, w)),
413 const Shape(Size(466, 678)),
414 const Shape(Size(626, 890)),
415 Shape(
416 const Size(626, 890),
417 features: [crease(const Rect.fromLTWH(313, 0, 0, 890))],
418 ),
419 Shape(
420 const Size(890, 626),
421 features: [crease(const Rect.fromLTWH(0, 313, 890, 0))],
422 ),
423 Shape(
424 const Size(900, 700),
425 features: [
426 crease(
427 const Rect.fromLTWH(440, 0, 20, 700),
428 type: DisplayFeatureType.hinge,
429 state: DisplayFeatureState.unknown,
430 ),
431 ],
432 ),
433 const Shape(Size(390, 844)),
434 ];
435 for (final s in sequence) {
436 await apply(tester, s);
437 await tester.pump();
438 expect(tester.takeException(), null, reason: '$s');
439 expect(
440 tester.state(find.byType(DiagnosticsPage)),
441 same(diagnostics),
442 reason: '$s',
443 );
444 expect(find.text('draft.example.test'), findsOneWidget, reason: '$s');
445 final list = find.byType(ServersPage);
446 if (list.evaluate().isNotEmpty) {
447 if (servers == null) {
448 servers = tester.state(list);
449 await tester.enterText(
450 find.descendant(of: list, matching: find.byType(TextField)),
451 'search-draft',
452 );
453 await tester.pump();
454 }
455 // The list may be rebuilt after a fold hid it; the typed search stays.
456 expect(find.text('search-draft'), findsOneWidget, reason: '$s');
457 }
458 }
459 expect(servers, isNotNull, reason: 'server list was shown in two panes');
460 await closeWorkspace(tester, c);
461 },
462 );
464 testWidgets('server list offers desktop-compatible import and export', (
465 tester,
466 ) async {
467 addTearDown(() => reset(tester));
468 for (final size in const [
469 Size(240, 568),
470 Size(390, 844),
471 Size(1280, 800),
472 ]) {
473 await apply(tester, Shape(size));
474 final c = await openWorkspace(tester);
475 await tester.tap(find.byIcon(Icons.import_export));
476 await tester.pumpAndSettle();
477 expect(find.byIcon(Icons.file_open), findsOneWidget, reason: '$size');
478 expect(find.byIcon(Icons.ios_share), findsOneWidget, reason: '$size');
479 // Export with no profiles reports the problem instead of failing.
480 await tester.tap(find.byIcon(Icons.ios_share));
481 await tester.pumpAndSettle();
482 expect(find.byType(SnackBar), findsOneWidget, reason: '$size');
483 expect(tester.takeException(), null);
484 await closeWorkspace(tester, c);
485 }
486 });
488 testWidgets('a tap on empty space closes the software keyboard', (
489 tester,
490 ) async {
491 addTearDown(() => reset(tester));
492 await apply(tester, const Shape(Size(390, 844)));
493 final c = await openWorkspace(tester);
494 await go(tester, Icons.network_check);
495 await tester.tap(find.byType(TextField).first);
496 await tester.pump();
497 expect(FocusManager.instance.primaryFocus?.context?.widget, isNotNull);
498 expect(tester.testTextInput.isVisible, true);
499 // Non-interactive text in the diagnostics form.
500 await tester.tapAt(
501 tester.getCenter(find.textContaining('TCP ping measures').first),
502 );
503 await tester.pump();
504 expect(tester.testTextInput.isVisible, false);
505 await closeWorkspace(tester, c);
506 });
508 group('folds and hinges', () {
509 Future<void> expectSides(
510 WidgetTester tester,
511 Rect fold, {
512 required bool vertical,
513 }) async {
514 final list = tester.getRect(find.byType(ServersPage));
515 final tool = tester.getRect(find.byType(DiagnosticsPage));
516 expect(list.overlaps(fold.inflate(-.01)) && fold.width > 0, false);
517 if (vertical) {
518 expect(list.right, lessThanOrEqualTo(fold.left + .01));
519 expect(tool.left, greaterThanOrEqualTo(fold.right - .01));
520 } else {
521 expect(tool.bottom, lessThanOrEqualTo(fold.top + .01));
522 expect(list.top, greaterThanOrEqualTo(fold.bottom - .01));
523 }
524 // No tappable control may sit across the fold line.
525 for (final e
526 in find
527 .byWidgetPredicate(
528 (w) =>
529 w is ButtonStyleButton || w is IconButton || w is TextField,
530 )
531 .evaluate()) {
532 final box = e.renderObject! as RenderBox;
533 if (!box.attached || !box.hasSize) continue;
534 final whole = box.localToGlobal(Offset.zero) & box.size;
535 // Only the part visible inside a pane counts; scrolled-away parts are
536 // clipped by the pane.
537 final pane = whole.overlaps(list) ? list : tool;
538 if (!whole.overlaps(pane)) continue;
539 final r = whole.intersect(pane);
540 final crosses = vertical
541 ? r.left < fold.left - .5 && r.right > fold.right + .5
542 : r.top < fold.top - .5 && r.bottom > fold.bottom + .5;
543 expect(
544 crosses,
545 false,
546 reason: '${e.widget.runtimeType} $r crosses $fold',
547 );
548 }
549 }
551 for (final ratio in [2.0, 3.0]) {
552 // iPhone Duo: Apple publishes physical pixels only (outer 1398 × 2034,
553 // inner 1878 × 2670). The display scale is not published and no
554 // iPhone Duo simulator is installed, so both common scales are tested
555 // as assumptions; logical size = physical / ratio.
556 final outer = const Size(1398, 2034) / ratio;
557 final inner = const Size(1878, 2670) / ratio;
558 testWidgets(
559 'iPhone Duo physical resolutions at @${ratio}x: closed, open, half folded, both orientations',
560 (tester) async {
561 addTearDown(() => reset(tester));
562 await apply(tester, Shape(outer, ratio: ratio));
563 final c = await openWorkspace(tester);
564 await go(tester, Icons.network_check);
565 expect(tester.view.physicalSize, const Size(1398, 2034));
566 for (final s in [outer, outer.flipped, inner, inner.flipped, outer]) {
567 await apply(
568 tester,
569 Shape(
570 s,
571 ratio: ratio,
572 padding: const EdgeInsets.only(top: 50, bottom: 20),
573 ),
574 );
575 expect(tester.takeException(), null, reason: '$s');
576 expect(WindowClass.ofWidth(s.width), AdaptiveLayout(s).windowClass);
577 }
578 // Half folded: the reserved region in the middle of the inner display,
579 // for a crease along either axis.
580 for (final s in [inner, inner.flipped]) {
581 for (final vertical in [true, false]) {
582 final fold = vertical
583 ? Rect.fromLTWH(s.width / 2 - 8, 0, 16, s.height)
584 : Rect.fromLTWH(0, s.height / 2 - 8, s.width, 16);
585 await apply(
586 tester,
587 Shape(s, ratio: ratio, features: [crease(fold)]),
588 );
589 await tester.pump();
590 expect(tester.takeException(), null, reason: '$s $fold');
591 await expectSides(tester, fold, vertical: vertical);
592 }
593 }
594 await closeWorkspace(tester, c);
595 },
596 );
597 }
599 testWidgets('book fold, tabletop fold, hinge and flat fold', (
600 tester,
601 ) async {
602 addTearDown(() => reset(tester));
603 await apply(tester, const Shape(Size(673, 841)));
604 final c = await openWorkspace(tester);
605 await go(tester, Icons.network_check);
606 // Closed foldable (narrow outer screen) and open inner screen.
607 for (final s in const [
608 Size(344, 882),
609 Size(673, 841),
610 Size(841, 673),
611 Size(373, 840),
612 Size(900, 1008),
613 ]) {
614 await apply(tester, Shape(s, ratio: 2.625));
615 expect(tester.takeException(), null, reason: '$s');
616 }
617 final book = crease(const Rect.fromLTWH(336, 0, 0, 841));
618 await apply(
619 tester,
620 Shape(const Size(673, 841), ratio: 2.625, features: [book]),
621 );
622 await tester.pump();
623 await expectSides(tester, book.bounds, vertical: true);
624 final table = crease(const Rect.fromLTWH(0, 420, 841, 0));
625 await apply(
626 tester,
627 Shape(const Size(841, 673), ratio: 2.625, features: [table]),
628 );
629 await tester.pump();
630 expect(find.byType(NavigationBar), findsOneWidget);
631 await expectSides(tester, table.bounds, vertical: false);
632 final hinge = crease(
633 const Rect.fromLTWH(540, 0, 34, 720),
634 type: DisplayFeatureType.hinge,
635 state: DisplayFeatureState.unknown,
636 );
637 await apply(
638 tester,
639 Shape(const Size(1114, 720), ratio: 2.5, features: [hinge]),
640 );
641 await tester.pump();
642 await expectSides(tester, hinge.bounds, vertical: true);
643 // A fully open (flat) fold is one continuous surface.
644 await apply(
645 tester,
646 Shape(
647 const Size(673, 841),
648 features: [
649 crease(
650 const Rect.fromLTWH(336, 0, 0, 841),
651 state: DisplayFeatureState.postureFlat,
652 ),
653 ],
654 ),
655 );
656 expect(find.byType(ServersPage), findsNothing);
657 expect(
658 tester.getRect(find.byType(DiagnosticsPage)).width,
659 greaterThan(500),
660 );
661 await closeWorkspace(tester, c);
662 });
663 });
665 testWidgets('split screen and small windows on tablets', (tester) async {
666 addTearDown(() => reset(tester));
667 await apply(tester, const Shape(Size(1194, 834)));
668 final c = await openWorkspace(tester);
669 for (var page = 1; page < pageIcons.length; page++) {
670 await go(tester, pageIcons[page]);
671 // iPad / Android tablet: full, 2/3, 1/2, 1/3 split and slide-over widths,
672 // then free-form resizing step by step.
673 for (final s in [
674 const Size(1194, 834),
675 const Size(796, 834),
676 const Size(597, 834),
677 const Size(390, 834),
678 const Size(320, 693),
679 const Size(1366, 1024),
680 const Size(678, 1024),
681 const Size(507, 1024),
682 const Size(1280, 800),
683 const Size(640, 800),
684 const Size(1600, 1000),
685 const Size(2560, 1440),
686 for (var w = 300.0; w <= 1400; w += 137) Size(w, 760),
687 ]) {
688 await apply(tester, Shape(s, ratio: 2));
689 expect(tester.takeException(), null, reason: 'page $page $s');
690 }
691 }
692 await closeWorkspace(tester, c);
693 });
695 for (final s in const [
696 Size(320, 568),
697 Size(390, 844),
698 Size(844, 390),
699 Size(820, 1180),
700 Size(1440, 900),
701 ]) {
702 testWidgets('accessibility guidelines at ${s.width}x${s.height}', (
703 tester,
704 ) async {
705 addTearDown(() => reset(tester));
706 final semantics = tester.ensureSemantics();
707 await apply(tester, Shape(s));
708 final c = await openWorkspace(tester);
709 for (final mode in [ThemeMode.light, ThemeMode.dark]) {
710 c.theme = mode;
711 c.changed();
712 await tester.pumpAndSettle();
713 for (var page = 0; page < pageIcons.length; page++) {
714 await go(tester, pageIcons[page]);
715 await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
716 await expectLater(tester, meetsGuideline(iOSTapTargetGuideline));
717 await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
718 await expectLater(tester, meetsGuideline(textContrastGuideline));
719 }
720 }
721 await closeWorkspace(tester, c);
722 semantics.dispose();
723 });
724 }
SHA-256a45a209596381f871a5edd36a67c3b19eabfd06aebfc65a08de1fb1daba915c4

CH-J Proprietary Software License 1.14 · تُتاح الشيفرة المصدرية بموجب CH-J Proprietary Software License 1.14. وإتاحتها لا تغيّر شروط الترخيص ولا تمنح حقوقًا إضافية.

SHA-256 للأرشيف46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655
الملفات