diff --git a/test/builtin/claude/conversation_view_test.dart b/test/builtin/claude/conversation_view_test.dart index 1f480ff5..ae9d7842 100644 --- a/test/builtin/claude/conversation_view_test.dart +++ b/test/builtin/claude/conversation_view_test.dart @@ -14,6 +14,8 @@ import 'package:clide/builtin/claude/src/conversation_view.dart'; import 'package:clide/builtin/claude/src/image_thumbnail.dart'; import 'package:clide/builtin/claude/src/transcript_publisher.dart'; import 'package:clide/builtin/claude/src/transcript_reader.dart'; +import 'package:clide/widgets/src/draw/drawing_card.dart' show DrawingCard; +import 'package:clide/widgets/src/svg/svg_painter.dart' show SvgView; import 'package:clide/builtin/claude/src/workflow_run.dart'; import 'package:clide/clide.dart' show IpcResponse; import 'package:clide/kernel/kernel.dart' show PaneKeyNav; @@ -493,6 +495,24 @@ void main() { expect(find.text('48'), findsOneWidget); // largest strip sample }); + testWidgets('an icon entry + card colour are parsed and rendered (T-313)', (tester) async { + await pumpWith(tester, [ + _iconMsg([const IconEntry(codepoint: 0xe2a4, name: 'gear', label: 'S', color: '#e2b714')], color: '#888888'), + ]); + expect(find.text('S'), findsOneWidget); + }); + + testWidgets('tapping a data-lightbox drawing opens the zoom lightbox (T-318)', (tester) async { + await pumpWith(tester, [ + DrawingMessage(uuid: 'L', timestamp: _t, isSidechain: false, svg: ''), + ]); + await tester.tap(find.byType(DrawingCard)); + await tester.pumpAndSettle(); + // The lightbox opened — its SvgView renders in the dialog overlay (in + // addition to the card's own). + expect(find.byType(SvgView), findsWidgets); + }); + testWidgets('inject() drives a new image card into a live view (T-249)', (tester) async { final c = await pumpWith(tester, [_user('hi')]); expect(find.text('image'), findsNothing); diff --git a/test/builtin/claude/extension_commands_test.dart b/test/builtin/claude/extension_commands_test.dart index ee0ea328..8c0d3960 100644 --- a/test/builtin/claude/extension_commands_test.dart +++ b/test/builtin/claude/extension_commands_test.dart @@ -13,6 +13,7 @@ import 'package:clide/builtin/claude/src/session_orchestrator.dart' show activeS import 'package:clide/clide.dart'; import 'package:clide/extension/extension.dart'; import 'package:clide/kernel/kernel.dart'; +import 'package:clide/src/daemon/icon_commands.dart' show iconShowChannel; import 'package:clide/src/daemon/image_commands.dart' show imageShowChannel; import 'package:flutter_test/flutter_test.dart'; @@ -128,6 +129,20 @@ void main() { // receive the card and the CLI already acked at publish time. }); + test('an icon-show message parses entries, dropped silently with no session (T-313)', () async { + f.services.messages.publish('test', iconShowChannel, { + 'entries': [ + {'codepoint': 0xe2a4, 'name': 'gear', 'label': 'Settings', 'description': 'd', 'color': '#fff'}, + {'name': 'noCodepoint'}, // skipped — no int codepoint + ], + 'color': '#888', + }); + f.services.messages.publish('test', iconShowChannel, {'entries': const []}); // empty → early return + f.services.messages.publish('test', iconShowChannel, {'entries': 'notalist'}); // not a list → early return + await pumpEventQueue(); + // No live session — the entry parse ran; nothing to inject into. + }); + test('a project switch closes sessions that belong to the old root (T-269)', () async { f.services.events.emit(const ProjectOpened(path: '/repo-one')); await pumpEventQueue(); diff --git a/test/builtin/problems/problems_controller_test.dart b/test/builtin/problems/problems_controller_test.dart index 53798288..7b8c55c5 100644 --- a/test/builtin/problems/problems_controller_test.dart +++ b/test/builtin/problems/problems_controller_test.dart @@ -1,7 +1,11 @@ import 'package:clide/builtin/problems/src/problems_controller.dart'; +import 'package:clide/clide.dart'; +import 'package:clide/kernel/kernel.dart'; import 'package:clide/src/env/supporter_binaries.dart'; import 'package:flutter_test/flutter_test.dart'; +import '../../helpers/fake_ipc.dart'; + void main() { group('supporterToolProblems', () { test('flags a stale supporter-binary pin', () { @@ -21,4 +25,89 @@ void main() { expect(supporterToolProblems(null), isEmpty); }); }); + + group('ProblemsController.refresh', () { + late FakeDaemonClient ipc; + SupporterBinaries? saved; + + setUp(() { + saved = activeSupporterBinaries; + activeSupporterBinaries = null; // isolate from supporter-tool problems + ipc = FakeDaemonClient(log: Logger(), events: DaemonBus()); + }); + tearDown(() => activeSupporterBinaries = saved); + + IpcResponse ok(Map data) => IpcResponse.ok(id: '1', data: data); + + test('a clean doctor + sync yields no problems', () async { + ipc.stub( + 'pql.doctor', + (_) async => ok({ + 'db': {'exists': true}, + 'skill': { + 'project': {'state': 'ok'}, + }, + }), + ); + ipc.stub('pql.decisions.sync', (_) async => ok({'broken': 0})); + final c = ProblemsController(ipc: ipc); + var notified = 0; + c.addListener(() => notified++); + await c.refresh(); + expect(c.problems, isEmpty); + expect(c.loading, isFalse); + expect(c.error, isNull); + expect(notified, greaterThan(0)); // loading toggled + final + }); + + test('flags a missing db, a stale skill, and broken refs', () async { + ipc.stub( + 'pql.doctor', + (_) async => ok({ + 'db': {'exists': false}, + 'skill': { + 'project': {'state': 'stale'}, + }, + }), + ); + ipc.stub('pql.decisions.sync', (_) async => ok({'broken': 2})); + final c = ProblemsController(ipc: ipc); + await c.refresh(); + final msgs = c.problems.map((p) => p.message).join('\n'); + expect(c.problems.map((p) => p.source), containsAll(['pql', 'decisions'])); + expect(msgs, contains('not found')); + expect(msgs, contains('stale')); + expect(msgs, contains('broken')); + }); + + test('a missing skill is flagged with the install hint', () async { + ipc.stub( + 'pql.doctor', + (_) async => ok({ + 'db': {'exists': true}, + 'skill': { + 'project': {'state': 'missing'}, + }, + }), + ); + ipc.stub('pql.decisions.sync', (_) async => ok({'broken': 0})); + final c = ProblemsController(ipc: ipc); + await c.refresh(); + expect(c.problems.any((p) => p.message.contains('not installed')), isTrue); + }); + + test('a failed doctor surfaces as a problem', () async { + ipc.stub( + 'pql.doctor', + (_) async => IpcResponse.err( + id: '1', + error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'boom'), + ), + ); + ipc.stub('pql.decisions.sync', (_) async => ok({'broken': 0})); + final c = ProblemsController(ipc: ipc); + await c.refresh(); + expect(c.problems.any((p) => p.message.contains('doctor failed')), isTrue); + }); + }); } diff --git a/test/builtin/tools_settings/extension_test.dart b/test/builtin/tools_settings/extension_test.dart index 4c69c9af..edadc21e 100644 --- a/test/builtin/tools_settings/extension_test.dart +++ b/test/builtin/tools_settings/extension_test.dart @@ -44,4 +44,17 @@ void main() { expect(f.services.settings.get('app.tools.detected'), isTrue); expect(activeSupporterBinaries, isNotNull); }); + + test('exposes its id, title, and version', () { + final ext = ToolsSettingsExtension(); + expect(ext.id, 'builtin.tools-settings'); + expect(ext.title, isNotEmpty); + expect(ext.version, isNotEmpty); + }); + + test('deactivate removes the live-sync listener', () async { + await f.services.extensions.deactivate('builtin.tools-settings'); + // Listener gone — a settings write no longer rebuilds the resolver, no throw. + await f.services.settings.set(supporterToolKey('d2'), '/x/d2'); + }); } diff --git a/test/daemon/draw_commands_test.dart b/test/daemon/draw_commands_test.dart index 6caa8c79..539bfa57 100644 --- a/test/daemon/draw_commands_test.dart +++ b/test/daemon/draw_commands_test.dart @@ -74,6 +74,21 @@ void main() { expect(published.single.data['svg'], ''); }); + test('an empty --file value is a userError', () async { + wire(); + final r = await draw(''); + expect(r.ok, isFalse); + expect(r.error?.kind, IpcErrorKind.userError); + }); + + test('valid JSON that is not an object is a userError', () async { + wire(); + files['arr.json'] = '[1,2,3]'; + final r = await draw('arr.json'); + expect(r.error?.kind, IpcErrorKind.userError); + expect(published, isEmpty); + }); + test('a missing file → notFound, nothing published', () async { wire(); final r = await draw('nope.json'); diff --git a/test/daemon/icon_commands_test.dart b/test/daemon/icon_commands_test.dart index 02c962cd..ee5e7cbd 100644 --- a/test/daemon/icon_commands_test.dart +++ b/test/daemon/icon_commands_test.dart @@ -84,6 +84,19 @@ void main() { expect(published.single.data['color'], 'red'); }); + test('an invalid card-level --color is an honest userError', () async { + wire(); + final r = await show(['gear'], flags: {'color': 'notacolor'}); + expect(r.error?.kind, IpcErrorKind.userError); + expect(published, isEmpty); + }); + + test('a missing --file is notFound', () async { + wire(); + final r = await show([], flags: {'file': 'gone.json'}); + expect(r.error?.kind, IpcErrorKind.notFound); + }); + test('an unknown glyph name is an honest userError', () async { wire(); final r = await show(['notaglyph']); @@ -97,6 +110,12 @@ void main() { expect(r.error?.kind, IpcErrorKind.userError); }); + test('an unknown icon inside a --file entry is a userError', () async { + wire(files: {'i.json': '[{"icon":"notaglyph"}]'}); + final r = await show([], flags: {'file': 'i.json'}); + expect(r.error?.kind, IpcErrorKind.userError); + }); + test('a malformed --file is a userError', () async { wire(files: {'i.json': 'not json'}); final r = await show([], flags: {'file': 'i.json'}); diff --git a/test/draw/d2_template_test.dart b/test/draw/d2_template_test.dart index 55e131fe..c3ef4c64 100644 --- a/test/draw/d2_template_test.dart +++ b/test/draw/d2_template_test.dart @@ -55,5 +55,18 @@ void main() { final r = await compile(run: (exe, src) async => throw 'ENOENT'); expect((r as DrawErr).message, contains('could not run d2')); }); + + test('spawns the resolved binary over stdin — real process (covers _spawnD2)', () async { + // /bin/cat stands in for d2: `cat - -` echoes stdin (the source) to stdout. + final r = await d2CompileViaBinary('hi', resolveD2: () => '/bin/cat'); + expect((r as DrawOk).svg, contains('hi')); + }, testOn: 'linux || mac-os'); + + test('the default resolver runs when resolveD2 is not injected', () async { + // run is injected so there is no real spawn; the default resolver either + // finds d2 or not — either way exercises _defaultResolveD2. + final r = await d2CompileViaBinary('a -> b', run: (exe, src) async => (code: 0, out: '', err: '')); + expect(r, anyOf(isA(), isA())); + }); }); } diff --git a/test/draw/draw_dispatch_test.dart b/test/draw/draw_dispatch_test.dart index a9b22352..7d8555d5 100644 --- a/test/draw/draw_dispatch_test.dart +++ b/test/draw/draw_dispatch_test.dart @@ -8,6 +8,13 @@ void main() { Future resolve(DrawingCardDoc doc, DrawingRegistry reg, {Map files = const {}}) => resolveDrawingSvg(doc, reg, readFile: reader(files)); + test('a fresh registry is empty; registering a handler fills it', () { + expect(DrawingRegistry().isEmpty, isTrue); + final reg = DrawingRegistry()..register('x', (_) async => const DrawOk('')); + expect(reg.isEmpty, isFalse); + expect(reg.handlerFor('x'), isNotNull); + }); + group('resolveDrawingSvg', () { test('primitive: inline svg passes through', () async { final r = await resolve(parseDrawingCardDoc({'svg': ''})!, DrawingRegistry()); diff --git a/test/draw/graph_template_test.dart b/test/draw/graph_template_test.dart index 9f5bc4c1..57a2ad77 100644 --- a/test/draw/graph_template_test.dart +++ b/test/draw/graph_template_test.dart @@ -54,6 +54,20 @@ void main() { expect((r as DrawErr).message, contains('duplicate')); }); + test('an edge from an unknown node is an error', () async { + final r = await handler( + doc({ + 'nodes': [ + {'id': 'a'}, + ], + 'edges': [ + {'from': 'ghost', 'to': 'a'}, + ], + }), + ); + expect((r as DrawErr).message, contains('ghost')); + }); + test('an edge to an unknown node is an error', () async { final r = await handler( doc({ diff --git a/test/svg/svg_document_test.dart b/test/svg/svg_document_test.dart index 7b55a1f2..95763be8 100644 --- a/test/svg/svg_document_test.dart +++ b/test/svg/svg_document_test.dart @@ -155,5 +155,40 @@ void main() { test('a group with data-label is skipped — annotations anchor leaf shapes', () { expect(buildSvgDocument('').annotations, isEmpty); }); + + test('computes a bounding box for every leaf shape type (T-318)', () { + final d = buildSvgDocument( + '' + '' + '' + '' + '' + 'hi' + '', + ); + expect(d.annotations.map((a) => a.label), containsAll(['e', 'l', 'p', 'pa', 't'])); + // ellipse bbox = [cx-rx, cy-ry, 2rx, 2ry]. + final e = d.annotations.firstWhere((a) => a.label == 'e'); + expect([e.x, e.y, e.width, e.height], [30, 30, 40, 20]); + }); + }); + + group('buildSvgDocument — style attribute vocabulary', () { + test('parses every stroke/text style branch', () { + // Exercises _cap/_join/_anchor/_baseline/_weight/_dash. + final kids = buildSvgDocument( + '' + '' + '' + 'a' + 'b' + '', + ).root.children; + expect(kids, hasLength(4)); // all parsed; every style branch hit + }); + + test('stroke-dasharray="none" yields no dashes', () { + expect(buildSvgDocument('').root.children, hasLength(1)); + }); }); } diff --git a/test/widgets/svg/svg_painter_test.dart b/test/widgets/svg/svg_painter_test.dart index 032ff4d1..d128b64e 100644 --- a/test/widgets/svg/svg_painter_test.dart +++ b/test/widgets/svg/svg_painter_test.dart @@ -135,4 +135,34 @@ void main() { } expect(anyInk, isTrue); }); + + const markerDefs = + ''; + + test('renders quad/arc/close path segments with markers (T-320)', () async { + // Each path leads with a different first-draw op to exercise every + // start-angle branch in the marker placement + the segPath builder. + for (final d in ['M5 25 Q20 5 35 25 Z', 'M5 25 A8 8 0 0 1 50 25', 'M5 25 Z']) { + final svg = '$markerDefs'; + final img = await render(svg, 100, 50); + expect(img.width, 100); + } + }); + + test('a line carries start + end markers (T-320)', () async { + final svg = '$markerDefs'; + final img = await render(svg, 50, 50); + expect(img.width, 50); + }); + + test('renders text anchors/baselines/weights + a fill-opacity (T-320)', () async { + const svg = + '' + '' + 'M' + 'e' + ''; + final img = await render(svg, 40, 20); + expect(img.width, 40); // exercised the anchor/baseline/weight + fill-opacity color paths + }); }