Skip to content

Commit 3618ac1

Browse files
committed
[red] Require numbered adjustable editor
Signed-off-by: Viwat Vchirawongkwin <viwat.v@chula.ac.th>
1 parent a290fd6 commit 3618ac1

5 files changed

Lines changed: 318 additions & 24 deletions

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// SPDX-License-Identifier: MIT
2+
// Part of PyBLE (https://pyble.dev) — see /LICENSE.
3+
4+
import 'package:flutter_riverpod/flutter_riverpod.dart';
5+
import 'package:flutter_test/flutter_test.dart';
6+
7+
import 'package:pyble/editor/editor.dart';
8+
9+
void main() {
10+
group('editor display settings', () {
11+
late ProviderContainer container;
12+
13+
setUp(() {
14+
container = ProviderContainer();
15+
addTearDown(container.dispose);
16+
});
17+
18+
test('starts at the Signal code size and changes one point at a time', () {
19+
expect(kEditorDefaultFontSize, 14);
20+
expect(
21+
container.read(editorDisplaySettingsProvider).fontSize,
22+
kEditorDefaultFontSize,
23+
);
24+
25+
container.read(editorDisplaySettingsProvider.notifier).increaseFontSize();
26+
expect(container.read(editorDisplaySettingsProvider).fontSize, 15);
27+
28+
container.read(editorDisplaySettingsProvider.notifier).decreaseFontSize();
29+
expect(container.read(editorDisplaySettingsProvider).fontSize, 14);
30+
});
31+
32+
test('clamps invalid input and disables movement beyond either bound', () {
33+
final EditorDisplaySettingsController controller = container.read(
34+
editorDisplaySettingsProvider.notifier,
35+
);
36+
37+
controller.setFontSize(double.negativeInfinity);
38+
expect(
39+
container.read(editorDisplaySettingsProvider).fontSize,
40+
kEditorDefaultFontSize,
41+
reason: 'non-finite values fall back without poisoning layout',
42+
);
43+
44+
controller.setFontSize(kEditorMinFontSize - 100);
45+
expect(
46+
container.read(editorDisplaySettingsProvider).fontSize,
47+
kEditorMinFontSize,
48+
);
49+
controller.decreaseFontSize();
50+
expect(
51+
container.read(editorDisplaySettingsProvider).fontSize,
52+
kEditorMinFontSize,
53+
);
54+
55+
controller.setFontSize(kEditorMaxFontSize + 100);
56+
expect(
57+
container.read(editorDisplaySettingsProvider).fontSize,
58+
kEditorMaxFontSize,
59+
);
60+
controller.increaseFontSize();
61+
expect(
62+
container.read(editorDisplaySettingsProvider).fontSize,
63+
kEditorMaxFontSize,
64+
);
65+
});
66+
67+
test('is app-session state rather than file or board state', () {
68+
final EditorDisplaySettingsController controller = container.read(
69+
editorDisplaySettingsProvider.notifier,
70+
);
71+
controller.setFontSize(19);
72+
73+
container
74+
.read(editorDocumentProvider.notifier)
75+
.openFromBoard(path: '/first.py', content: 'first\n');
76+
container.read(editorDocumentProvider.notifier).newDocument();
77+
78+
expect(container.read(editorDisplaySettingsProvider).fontSize, 19);
79+
expect(container.read(editorDocumentProvider).name, 'untitled.py');
80+
});
81+
});
82+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// SPDX-License-Identifier: MIT
2+
// Part of PyBLE (https://pyble.dev) — see /LICENSE.
3+
4+
import 'dart:io';
5+
6+
import 'package:flutter_test/flutter_test.dart';
7+
8+
import '../support/repo_paths.dart';
9+
10+
void main() {
11+
test('the editor cannot send source to an HTTP analyzer', () {
12+
final Directory editor = Directory('${appPackageRoot().path}/lib/editor');
13+
final List<File> dartFiles = editor
14+
.listSync(recursive: true)
15+
.whereType<File>()
16+
.where((File file) => file.path.endsWith('.dart'))
17+
.toList(growable: false);
18+
19+
expect(dartFiles, isNotEmpty);
20+
for (final File file in dartFiles) {
21+
final String source = file.readAsStringSync();
22+
expect(source, isNot(contains("package:http")), reason: file.path);
23+
expect(source, isNot(contains('DartPadAnalyzer')), reason: file.path);
24+
}
25+
});
26+
}

‎app/test/widget/editor_smart_punctuation_test.dart‎

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
//
44
// A-25 — end-to-end cover through the REAL EditorView widget.
55
//
6-
// The unit tests pin the formatter in isolation; this pins that the editor
7-
// actually WIRES it, so a future refactor cannot quietly drop `inputFormatters`
8-
// and re-introduce `SyntaxError: invalid syntax` on a tablet.
6+
// The unit tests pin the formatter in isolation; this pins its observable
7+
// guarantee through both EditorSurface implementations, independent of where
8+
// each adapter intercepts TextEditingValue updates.
99

1010
import 'package:flutter/material.dart';
11+
import 'package:flutter_riverpod/flutter_riverpod.dart';
1112
import 'package:flutter_test/flutter_test.dart';
1213

1314
import 'package:pyble/editor/editor.dart';
@@ -17,28 +18,28 @@ import '../support/feature_harness.dart';
1718
import '../support/recording_connection.dart';
1819

1920
void main() {
20-
testWidgets('the editor field carries the smart-punctuation formatter', (
21+
testWidgets('both editor adapters preserve the typing-safety configuration', (
2122
WidgetTester tester,
2223
) async {
23-
await pumpSurface(
24-
tester,
25-
const EditorView(),
26-
connection: RecordingConnection(initial: ConnState.ready),
27-
);
28-
await tester.pumpAndSettle();
24+
for (final EditorSurfaceBuilder builder in <EditorSurfaceBuilder>[
25+
richEditorSurfaceBuilder,
26+
plainEditorSurfaceBuilder,
27+
]) {
28+
await pumpSurface(
29+
tester,
30+
const EditorView(),
31+
connection: RecordingConnection(initial: ConnState.ready),
32+
extra: <Override>[
33+
editorSurfaceBuilderProvider.overrideWithValue(builder),
34+
],
35+
);
36+
await tester.pumpAndSettle();
2937

30-
final TextField field = tester.widget<TextField>(find.byType(TextField));
31-
expect(
32-
field.inputFormatters?.whereType<SmartPunctuationFormatter>(),
33-
isNotEmpty,
34-
reason:
35-
'without this the iOS-only smartQuotesType hint is the ONLY '
36-
'defence, and it covers neither Android/macOS nor pasted text',
37-
);
38-
// The iOS hints must remain too — belt and braces.
39-
expect(field.smartQuotesType, SmartQuotesType.disabled);
40-
expect(field.smartDashesType, SmartDashesType.disabled);
41-
expect(field.autocorrect, isFalse);
38+
final TextField field = tester.widget<TextField>(find.byType(TextField));
39+
expect(field.smartQuotesType, SmartQuotesType.disabled);
40+
expect(field.smartDashesType, SmartDashesType.disabled);
41+
expect(field.autocorrect, isFalse);
42+
}
4243
});
4344

4445
testWidgets(
@@ -53,7 +54,10 @@ void main() {
5354

5455
// Exactly what iPadOS Smart Punctuation delivers, and exactly the bytes
5556
// recovered from the bench board's /test.py.
56-
await tester.enterText(find.byType(TextField), 'print(“Hello world!!”)');
57+
await tester.enterText(
58+
find.byType(EditableText),
59+
'print(“Hello world!!”)',
60+
);
5761
await tester.pumpAndSettle();
5862

5963
expect(
@@ -74,7 +78,7 @@ void main() {
7478
);
7579
await tester.pumpAndSettle();
7680

77-
await tester.enterText(find.byType(TextField), 'x = 1');
81+
await tester.enterText(find.byType(EditableText), 'x = 1');
7882
await tester.pumpAndSettle();
7983

8084
final String content = containerOf(
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// SPDX-License-Identifier: MIT
2+
// Part of PyBLE (https://pyble.dev) — see /LICENSE.
3+
4+
import 'package:flutter/material.dart';
5+
import 'package:flutter/services.dart';
6+
import 'package:flutter_test/flutter_test.dart';
7+
8+
import 'package:pyble/editor/editor.dart';
9+
import 'package:pyble/pble/pble.dart';
10+
11+
import '../support/feature_harness.dart';
12+
13+
void main() {
14+
testWidgets('Tab indents a multiline selection without deleting source', (
15+
WidgetTester tester,
16+
) async {
17+
final FakeConnection fake = FakeConnection(initial: ConnState.ready);
18+
addTearDown(fake.dispose);
19+
const String source = 'a = 1\nb = 2\nc = 3\n';
20+
21+
await pumpSurface(tester, const EditorView(), connection: fake);
22+
containerOf(tester)
23+
.read(editorDocumentProvider.notifier)
24+
.openFromBoard(path: '/main.py', content: source);
25+
await tester.pump();
26+
27+
final EditableText editable = tester.widget<EditableText>(
28+
find.byType(EditableText),
29+
);
30+
editable.controller.selection = const TextSelection(
31+
baseOffset: 0,
32+
extentOffset: 12,
33+
);
34+
editable.focusNode.requestFocus();
35+
await tester.pump();
36+
37+
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
38+
await tester.pump();
39+
40+
expect(
41+
containerOf(tester).read(editorDocumentProvider).content,
42+
' a = 1\n b = 2\nc = 3\n',
43+
reason: 'the selected source must be indented, never replaced by spaces',
44+
);
45+
});
46+
}

‎app/test/widget/editor_view_test.dart‎

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,137 @@ const Key _newKey = ValueKey<String>('editorNewButton');
2929

3030
void main() {
3131
group('A-20 EditorView — editable surface', () {
32+
testWidgets('shows one-based logical line numbers including a blank tail', (
33+
WidgetTester tester,
34+
) async {
35+
final FakeConnection fake = FakeConnection(initial: ConnState.ready);
36+
addTearDown(fake.dispose);
37+
await pumpSurface(tester, const EditorView(), connection: fake);
38+
39+
containerOf(tester)
40+
.read(editorDocumentProvider.notifier)
41+
.openFromBoard(path: '/main.py', content: 'alpha\nbeta\n');
42+
await tester.pump();
43+
44+
expect(find.byKey(kEditorRichSurfaceKey), findsOneWidget);
45+
expect(find.text('1'), findsOneWidget);
46+
expect(find.text('2'), findsOneWidget);
47+
expect(
48+
find.text('3'),
49+
findsOneWidget,
50+
reason: 'a trailing newline creates a numbered blank logical line',
51+
);
52+
53+
await tester.enterText(find.byType(EditableText), 'alpha');
54+
await tester.pump();
55+
expect(find.text('1'), findsOneWidget);
56+
expect(find.text('2'), findsNothing);
57+
expect(find.text('3'), findsNothing);
58+
});
59+
60+
testWidgets('font controls resize code and gutter without editing', (
61+
WidgetTester tester,
62+
) async {
63+
final FakeConnection fake = FakeConnection(initial: ConnState.ready);
64+
addTearDown(fake.dispose);
65+
await pumpSurface(tester, const EditorView(), connection: fake);
66+
containerOf(tester)
67+
.read(editorDocumentProvider.notifier)
68+
.openFromBoard(path: '/main.py', content: 'alpha\nbeta');
69+
await tester.pump();
70+
71+
final EditableText before = tester.widget<EditableText>(
72+
find.byType(EditableText),
73+
);
74+
before.controller.selection = const TextSelection.collapsed(offset: 3);
75+
final EditorDocument documentBefore = containerOf(
76+
tester,
77+
).read(editorDocumentProvider);
78+
79+
expect(before.style.fontSize, kEditorDefaultFontSize);
80+
expect(tester.widget<Text>(find.text('1')).style?.fontSize, 14);
81+
82+
await tester.tap(find.byKey(kEditorIncreaseFontSizeButtonKey));
83+
await tester.pump();
84+
85+
final EditableText after = tester.widget<EditableText>(
86+
find.byType(EditableText),
87+
);
88+
expect(after.style.fontSize, 15);
89+
expect(tester.widget<Text>(find.text('1')).style?.fontSize, 15);
90+
expect(
91+
after.controller.selection,
92+
const TextSelection.collapsed(offset: 3),
93+
);
94+
expect(
95+
containerOf(tester).read(editorDocumentProvider),
96+
same(documentBefore),
97+
);
98+
});
99+
100+
testWidgets('font buttons are localized and disable at their bounds', (
101+
WidgetTester tester,
102+
) async {
103+
final FakeConnection fake = FakeConnection(initial: ConnState.ready);
104+
addTearDown(fake.dispose);
105+
await pumpSurface(tester, const EditorView(), connection: fake);
106+
final AppLocalizations l10n = l10nOf(tester);
107+
108+
expect(
109+
tester
110+
.widget<IconButton>(find.byKey(kEditorDecreaseFontSizeButtonKey))
111+
.tooltip,
112+
l10n.editorDecreaseFontSize,
113+
);
114+
expect(
115+
tester
116+
.widget<IconButton>(find.byKey(kEditorIncreaseFontSizeButtonKey))
117+
.tooltip,
118+
l10n.editorIncreaseFontSize,
119+
);
120+
121+
containerOf(tester)
122+
.read(editorDisplaySettingsProvider.notifier)
123+
.setFontSize(kEditorMinFontSize);
124+
await tester.pump();
125+
expect(
126+
tester
127+
.widget<IconButton>(find.byKey(kEditorDecreaseFontSizeButtonKey))
128+
.onPressed,
129+
isNull,
130+
);
131+
132+
containerOf(tester)
133+
.read(editorDisplaySettingsProvider.notifier)
134+
.setFontSize(kEditorMaxFontSize);
135+
await tester.pump();
136+
expect(
137+
tester
138+
.widget<IconButton>(find.byKey(kEditorIncreaseFontSizeButtonKey))
139+
.onPressed,
140+
isNull,
141+
);
142+
});
143+
144+
testWidgets('compact editor exposes font sizing without overflowing', (
145+
WidgetTester tester,
146+
) async {
147+
final FakeConnection fake = FakeConnection(initial: ConnState.ready);
148+
addTearDown(fake.dispose);
149+
await pumpSurface(
150+
tester,
151+
const EditorView(),
152+
connection: fake,
153+
size: const Size(390, 844),
154+
);
155+
156+
expect(find.byKey(kEditorCompactFontSizeButtonKey), findsOneWidget);
157+
expect(tester.takeException(), isNull);
158+
await tester.tap(find.byKey(kEditorCompactFontSizeButtonKey));
159+
await tester.pumpAndSettle();
160+
expect(find.byKey(kEditorCompactFontSizeIncreaseKey), findsOneWidget);
161+
});
162+
32163
testWidgets('exposes the app-layer Python to Blocks action', (
33164
WidgetTester tester,
34165
) async {
@@ -73,6 +204,11 @@ void main() {
73204
findsOneWidget,
74205
reason: 'the empty buffer shows the localized MicroPython hint',
75206
);
207+
expect(
208+
find.text('1'),
209+
findsOneWidget,
210+
reason: 'the empty buffer still has logical line one',
211+
);
76212
});
77213

78214
testWidgets('typing updates the current document and marks it dirty', (

0 commit comments

Comments
 (0)