feat: unified shell, diagnostics pack, native Office, sticky board
All checks were successful
CI / Windows build (push) Successful in 14m22s
All checks were successful
CI / Windows build (push) Successful in 14m22s
Make Surface remote debugging and classroom workflows viable: always-on structured logs with one-click zip export, a single AppShell chrome, OOXML PPTX/DOCX annotation without LibreOffice, and a first-class sticky board. Also drop spike/legacy ink widgets and tighten pen feel (predictor, PenInfoHistory, page-tile layer). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
36
test/diagnostics_test.dart
Normal file
36
test/diagnostics_test.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:badnote/diagnostics/badnote_log.dart';
|
||||
import 'package:badnote/diagnostics/pen_event_ring.dart';
|
||||
import 'package:badnote/diagnostics/frame_sampler.dart';
|
||||
|
||||
void main() {
|
||||
test('PenEventRing retains recent events', () {
|
||||
final ring = PenEventRing.instance;
|
||||
ring.clear();
|
||||
ring.recordArbiter(
|
||||
activeCount: 1,
|
||||
deviceKind: 'stylus',
|
||||
draw: true,
|
||||
fingerDrawing: false,
|
||||
);
|
||||
expect(ring.recent(), isNotEmpty);
|
||||
expect(ring.toJsonList().first['decision'], 'draw');
|
||||
});
|
||||
|
||||
test('FrameSampler flags over-budget frames', () {
|
||||
FrameSampler.instance.reset();
|
||||
FrameSampler.instance.record('test', 8);
|
||||
FrameSampler.instance.record('slow', 30);
|
||||
expect(FrameSampler.instance.overBudget, 1);
|
||||
expect(FrameSampler.instance.summary()['total'], 2);
|
||||
});
|
||||
|
||||
test('BadNoteLog ring accepts entries without start', () {
|
||||
BadNoteLog.instance.info(LogSubsystem.diag, 'unit_test_ping');
|
||||
expect(
|
||||
BadNoteLog.instance.snapshotRing().any((e) => e['msg'] == 'unit_test_ping'),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
}
|
||||
66
test/office/office_parser_test.dart
Normal file
66
test/office/office_parser_test.dart
Normal file
@@ -0,0 +1,66 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:archive/archive.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:badnote/services/office/docx_parser.dart';
|
||||
import 'package:badnote/services/office/pptx_parser.dart';
|
||||
|
||||
ArchiveFile _xml(String name, String body) {
|
||||
final bytes = Uint8List.fromList(body.codeUnits);
|
||||
return ArchiveFile(name, bytes.length, bytes);
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('PptxParser extracts slide text from minimal OOXML', () async {
|
||||
final archive = Archive();
|
||||
archive.addFile(_xml(
|
||||
'[Content_Types].xml',
|
||||
'<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"></Types>',
|
||||
));
|
||||
archive.addFile(_xml(
|
||||
'ppt/presentation.xml',
|
||||
'<?xml version="1.0"?><p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">'
|
||||
'<p:sldSz cx="12192000" cy="6858000"/></p:presentation>',
|
||||
));
|
||||
archive.addFile(_xml(
|
||||
'ppt/slides/slide1.xml',
|
||||
'<?xml version="1.0"?><p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" '
|
||||
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">'
|
||||
'<p:cSld><p:spTree><p:sp><p:txBody><a:p><a:r><a:t>Hello Slide</a:t></a:r></a:p></p:txBody></p:sp>'
|
||||
'</p:spTree></p:cSld></p:sld>',
|
||||
));
|
||||
|
||||
final encoded = ZipEncoder().encode(archive)!;
|
||||
final dir = await Directory.systemTemp.createTemp('pptx_test_');
|
||||
final path = '${dir.path}/t.pptx';
|
||||
await File(path).writeAsBytes(encoded);
|
||||
|
||||
final parsed = await PptxParser().parse(path);
|
||||
expect(parsed.slides, isNotEmpty);
|
||||
expect(parsed.plainText, contains('Hello Slide'));
|
||||
await dir.delete(recursive: true);
|
||||
});
|
||||
|
||||
test('DocxParser extracts paragraphs from minimal OOXML', () async {
|
||||
final archive = Archive();
|
||||
archive.addFile(_xml(
|
||||
'[Content_Types].xml',
|
||||
'<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"></Types>',
|
||||
));
|
||||
archive.addFile(_xml(
|
||||
'word/document.xml',
|
||||
'<?xml version="1.0"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">'
|
||||
'<w:body><w:p><w:r><w:t>Linear Algebra</w:t></w:r></w:p></w:body></w:document>',
|
||||
));
|
||||
final encoded = ZipEncoder().encode(archive)!;
|
||||
final dir = await Directory.systemTemp.createTemp('docx_test_');
|
||||
final path = '${dir.path}/t.docx';
|
||||
await File(path).writeAsBytes(encoded);
|
||||
|
||||
final parsed = await DocxParser().parse(path);
|
||||
expect(parsed.plainText, contains('Linear Algebra'));
|
||||
await dir.delete(recursive: true);
|
||||
});
|
||||
}
|
||||
42
test/page_tile_layer_test.dart
Normal file
42
test/page_tile_layer_test.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:badnote/editor/pdf/page_tile_cache.dart';
|
||||
import 'package:badnote/editor/pdf/page_tile_layer.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
test('PageTileCache LRU evicts oldest', () async {
|
||||
final cache = PageTileCache(maxTiles: 2);
|
||||
final a = await _tinyImage();
|
||||
final b = await _tinyImage();
|
||||
final c = await _tinyImage();
|
||||
cache.put(const TileKey('p1', 96), a);
|
||||
cache.put(const TileKey('p2', 96), b);
|
||||
expect(cache.length, 2);
|
||||
cache.put(const TileKey('p3', 96), c);
|
||||
expect(cache.length, 2);
|
||||
expect(cache.get(const TileKey('p1', 96)), isNull);
|
||||
});
|
||||
|
||||
test('PageTileLayer falls back to last good', () async {
|
||||
final layer = PageTileLayer(cache: PageTileCache(maxTiles: 4));
|
||||
final img = await _tinyImage();
|
||||
layer.put(const TileKey('p1', 96), img);
|
||||
expect(layer.resolve(const TileKey('p1', 192)), same(img));
|
||||
// Don't call dispose() in unit test — cache dispose needs a frame.
|
||||
});
|
||||
}
|
||||
|
||||
Future<ui.Image> _tinyImage() async {
|
||||
final recorder = ui.PictureRecorder();
|
||||
final canvas = ui.Canvas(recorder);
|
||||
canvas.drawRect(
|
||||
const ui.Rect.fromLTWH(0, 0, 2, 2),
|
||||
ui.Paint()..color = const ui.Color(0xFFFFFFFF),
|
||||
);
|
||||
final picture = recorder.endRecording();
|
||||
return picture.toImage(2, 2);
|
||||
}
|
||||
28
test/stroke_predictor_test.dart
Normal file
28
test/stroke_predictor_test.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:badnote/editor/engine/stroke_predictor.dart';
|
||||
|
||||
void main() {
|
||||
test('StrokePredictor projects ahead when velocity is high', () {
|
||||
final p = StrokePredictor(lookaheadMs: 16);
|
||||
final t0 = DateTime.utc(2026, 1, 1, 0, 0, 0, 0);
|
||||
expect(p.observe(const Offset(0, 0), 0.5, at: t0), isNull);
|
||||
final tip = p.observe(
|
||||
const Offset(10, 0),
|
||||
0.5,
|
||||
at: t0.add(const Duration(milliseconds: 8)),
|
||||
);
|
||||
// 10px / 8ms ≈ 1250 px/s → 16ms lookahead ≈ 20px ahead
|
||||
expect(tip, isNotNull);
|
||||
expect(tip!.offset.dx, greaterThan(10));
|
||||
});
|
||||
|
||||
test('StrokePredictor resets cleanly', () {
|
||||
final p = StrokePredictor();
|
||||
p.observe(const Offset(0, 0), 0.4);
|
||||
p.reset();
|
||||
expect(p.observe(const Offset(1, 1), 0.4), isNull);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user