mirror of
https://github.com/foss42/apidash.git
synced 2025-12-10 07:08:08 +08:00
feat: implement search highlighting across terminal tiles and expandable sections
This commit is contained in:
147
test/widgets/expandable_section_test.dart
Normal file
147
test/widgets/expandable_section_test.dart
Normal file
@@ -0,0 +1,147 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:apidash/widgets/expandable_section.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('ExpandableSection starts collapsed by default', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: ExpandableSection(
|
||||
title: 'Test Section',
|
||||
child: Text('Content'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Test Section'), findsOneWidget);
|
||||
expect(find.text('Content'), findsNothing);
|
||||
expect(find.byIcon(Icons.keyboard_arrow_down), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('ExpandableSection can be expanded by tapping', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: ExpandableSection(
|
||||
title: 'Test Section',
|
||||
child: Text('Content'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Tap to expand
|
||||
await tester.tap(find.byType(InkWell));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Test Section'), findsOneWidget);
|
||||
expect(find.text('Content'), findsOneWidget);
|
||||
expect(find.byIcon(Icons.keyboard_arrow_up), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('ExpandableSection respects initiallyOpen parameter',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: ExpandableSection(
|
||||
title: 'Test Section',
|
||||
initiallyOpen: true,
|
||||
child: Text('Content'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Test Section'), findsOneWidget);
|
||||
expect(find.text('Content'), findsOneWidget);
|
||||
expect(find.byIcon(Icons.keyboard_arrow_up), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('ExpandableSection respects forceOpen parameter', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: ExpandableSection(
|
||||
title: 'Test Section',
|
||||
forceOpen: true,
|
||||
child: Text('Content'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Test Section'), findsOneWidget);
|
||||
expect(find.text('Content'), findsOneWidget);
|
||||
expect(find.byIcon(Icons.keyboard_arrow_up), findsOneWidget);
|
||||
|
||||
// Tapping should not close it when forceOpen is true
|
||||
await tester.tap(find.byType(InkWell));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Content'), findsOneWidget);
|
||||
expect(find.byIcon(Icons.keyboard_arrow_up), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('ExpandableSection highlights title when highlightQuery matches',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: ExpandableSection(
|
||||
title: 'Network Settings',
|
||||
highlightQuery: 'network',
|
||||
child: Text('Content'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// When highlighting is applied, the title is rendered as RichText instead of Text
|
||||
final richTexts = tester.widgetList<RichText>(find.byType(RichText));
|
||||
final titleRichText = richTexts.firstWhere(
|
||||
(rt) => rt.text.toPlainText() == 'Network Settings',
|
||||
orElse: () => throw Exception('RichText with title not found'),
|
||||
);
|
||||
expect(titleRichText.text.toPlainText(), 'Network Settings');
|
||||
});
|
||||
|
||||
testWidgets('ExpandableSection updates when forceOpen changes',
|
||||
(tester) async {
|
||||
bool forceOpen = false;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: StatefulBuilder(
|
||||
builder: (context, setState) => Column(
|
||||
children: [
|
||||
ExpandableSection(
|
||||
title: 'Test Section',
|
||||
forceOpen: forceOpen ? true : null,
|
||||
child: const Text('Content'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => setState(() => forceOpen = !forceOpen),
|
||||
child: const Text('Toggle Force Open'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Initially collapsed
|
||||
expect(find.text('Content'), findsNothing);
|
||||
|
||||
// Tap button to set forceOpen to true
|
||||
await tester.tap(find.text('Toggle Force Open'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Should now be open
|
||||
expect(find.text('Content'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
212
test/widgets/highlight_text_test.dart
Normal file
212
test/widgets/highlight_text_test.dart
Normal file
@@ -0,0 +1,212 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:apidash/widgets/highlight_text.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('HighlightedSelectableText renders plain text when no query',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: HighlightedSelectableText(
|
||||
text: 'Hello World',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Hello World'), findsOneWidget);
|
||||
expect(find.byType(SelectableText), findsOneWidget);
|
||||
|
||||
final selectableText =
|
||||
tester.widget<SelectableText>(find.byType(SelectableText));
|
||||
expect(selectableText.data, 'Hello World');
|
||||
});
|
||||
|
||||
testWidgets('HighlightedSelectableText renders plain text when empty query',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: HighlightedSelectableText(
|
||||
text: 'Hello World',
|
||||
query: '',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Hello World'), findsOneWidget);
|
||||
expect(find.byType(SelectableText), findsOneWidget);
|
||||
|
||||
final selectableText =
|
||||
tester.widget<SelectableText>(find.byType(SelectableText));
|
||||
expect(selectableText.data, 'Hello World');
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'HighlightedSelectableText renders with highlights when query matches',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: HighlightedSelectableText(
|
||||
text: 'Hello World',
|
||||
query: 'World',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// When highlighting is applied, it uses SelectableText.rich
|
||||
expect(find.byType(SelectableText), findsOneWidget);
|
||||
|
||||
final selectableText =
|
||||
tester.widget<SelectableText>(find.byType(SelectableText));
|
||||
// The text should still contain the original content
|
||||
expect(selectableText.textSpan?.toPlainText(), 'Hello World');
|
||||
});
|
||||
|
||||
testWidgets('HighlightedSelectableText is case insensitive', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: HighlightedSelectableText(
|
||||
text: 'Hello World',
|
||||
query: 'HELLO',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(SelectableText), findsOneWidget);
|
||||
|
||||
final selectableText =
|
||||
tester.widget<SelectableText>(find.byType(SelectableText));
|
||||
expect(selectableText.textSpan?.toPlainText(), 'Hello World');
|
||||
});
|
||||
|
||||
testWidgets('HighlightedSelectableText handles multiple matches',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: HighlightedSelectableText(
|
||||
text: 'Hello Hello World',
|
||||
query: 'Hello',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(SelectableText), findsOneWidget);
|
||||
|
||||
final selectableText =
|
||||
tester.widget<SelectableText>(find.byType(SelectableText));
|
||||
expect(selectableText.textSpan?.toPlainText(), 'Hello Hello World');
|
||||
});
|
||||
|
||||
testWidgets('HighlightedSelectableText applies custom style', (tester) async {
|
||||
const customStyle = TextStyle(fontSize: 20, color: Colors.red);
|
||||
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: HighlightedSelectableText(
|
||||
text: 'Hello World',
|
||||
style: customStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(SelectableText), findsOneWidget);
|
||||
|
||||
final selectableText =
|
||||
tester.widget<SelectableText>(find.byType(SelectableText));
|
||||
expect(selectableText.style, customStyle);
|
||||
});
|
||||
|
||||
testWidgets('buildHighlightedSpans returns plain TextSpan when no query',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
final spans = buildHighlightedSpans('Hello World', context, null);
|
||||
return RichText(
|
||||
text: TextSpan(children: spans),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(RichText), findsOneWidget);
|
||||
|
||||
final richText = tester.widget<RichText>(find.byType(RichText));
|
||||
expect(richText.text.toPlainText(), 'Hello World');
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'buildHighlightedSpans returns highlighted spans when query matches',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
final spans =
|
||||
buildHighlightedSpans('Hello World', context, 'World');
|
||||
return RichText(
|
||||
text: TextSpan(children: spans),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(RichText), findsOneWidget);
|
||||
|
||||
final richText = tester.widget<RichText>(find.byType(RichText));
|
||||
expect(richText.text.toPlainText(), 'Hello World');
|
||||
|
||||
// Should have multiple spans (before match, match, after match)
|
||||
final textSpan = richText.text as TextSpan;
|
||||
expect(textSpan.children, isNotNull);
|
||||
expect(textSpan.children!.length, greaterThan(1));
|
||||
});
|
||||
|
||||
testWidgets('buildHighlightedSpans applies custom base style',
|
||||
(tester) async {
|
||||
const baseStyle = TextStyle(fontSize: 18, color: Colors.blue);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
final spans = buildHighlightedSpans(
|
||||
'Hello World',
|
||||
context,
|
||||
'World',
|
||||
baseStyle: baseStyle,
|
||||
);
|
||||
return RichText(
|
||||
text: TextSpan(children: spans, style: baseStyle),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(RichText), findsOneWidget);
|
||||
|
||||
final richText = tester.widget<RichText>(find.byType(RichText));
|
||||
expect(richText.text.style, baseStyle);
|
||||
});
|
||||
}
|
||||
@@ -18,12 +18,30 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(home: Scaffold(body: SystemLogTile(entry: entry))));
|
||||
expect(find.textContaining('[ui] Updated'), findsOneWidget);
|
||||
// Stack is rendered using SelectableText
|
||||
// Stack is rendered using HighlightedSelectableText which internally uses SelectableText
|
||||
final stackSelectable =
|
||||
tester.widget<SelectableText>(find.byType(SelectableText).first);
|
||||
tester.widget<SelectableText>(find.byType(SelectableText).last);
|
||||
expect(stackSelectable.data, 'trace');
|
||||
});
|
||||
|
||||
testWidgets('SystemLogTile renders with search highlighting', (tester) async {
|
||||
final entry = TerminalEntry(
|
||||
id: 's1',
|
||||
source: TerminalSource.system,
|
||||
level: TerminalLevel.info,
|
||||
system:
|
||||
SystemLogData(category: 'networking', message: 'Request completed'),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SystemLogTile(entry: entry, searchQuery: 'network'))));
|
||||
|
||||
// Should find the highlighted text
|
||||
expect(
|
||||
find.textContaining('[networking] Request completed'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('JsLogTile composes body text with context and requestName',
|
||||
(tester) async {
|
||||
final entry = TerminalEntry(
|
||||
@@ -41,6 +59,31 @@ void main() {
|
||||
// Background color for warn renders but we only assert presence of text
|
||||
});
|
||||
|
||||
testWidgets('JsLogTile highlights search query in body text', (tester) async {
|
||||
final entry = TerminalEntry(
|
||||
id: 'j1',
|
||||
source: TerminalSource.js,
|
||||
level: TerminalLevel.info,
|
||||
js: JsLogData(
|
||||
level: 'info',
|
||||
args: ['API', 'response', 'success'],
|
||||
context: 'postRequest'),
|
||||
);
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Scaffold(
|
||||
body: JsLogTile(
|
||||
entry: entry, requestName: 'TestAPI', searchQuery: 'API'))));
|
||||
|
||||
// Should find the text containing API in SelectableText widgets
|
||||
// When highlighting is applied, the content is in textSpan
|
||||
final selectables =
|
||||
tester.widgetList<SelectableText>(find.byType(SelectableText)).toList();
|
||||
final hasAPIText = selectables.any((s) =>
|
||||
(s.data != null && s.data!.contains('API')) ||
|
||||
(s.textSpan != null && s.textSpan!.toPlainText().contains('API')));
|
||||
expect(hasAPIText, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('NetworkLogTile expands to show details and status/duration',
|
||||
(tester) async {
|
||||
final n = NetworkLogData(
|
||||
@@ -90,4 +133,39 @@ void main() {
|
||||
expect(
|
||||
selectables.any((w) => (w.data ?? '').contains('Status: 200')), isTrue);
|
||||
});
|
||||
|
||||
testWidgets('NetworkLogTile shows search query in URL', (tester) async {
|
||||
final n = NetworkLogData(
|
||||
phase: NetworkPhase.completed,
|
||||
apiType: APIType.rest,
|
||||
method: HTTPVerb.post,
|
||||
url: 'https://api.example.com/data',
|
||||
responseStatus: 201,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
requestHeaders: const {'Content-Type': 'application/json'},
|
||||
responseHeaders: const {'Server': 'nginx'},
|
||||
requestBodyPreview: '{"name": "test"}',
|
||||
responseBodyPreview: '{"id": 123}',
|
||||
);
|
||||
final entry = TerminalEntry(
|
||||
id: 'n1',
|
||||
source: TerminalSource.network,
|
||||
level: TerminalLevel.info,
|
||||
network: n);
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Scaffold(
|
||||
body: NetworkLogTile(
|
||||
entry: entry,
|
||||
requestName: 'TestAPI',
|
||||
searchQuery: 'example'))));
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// The RichText should contain the search query "example" in the URL
|
||||
final richTexts = tester.widgetList<RichText>(find.byType(RichText));
|
||||
final hasExampleText =
|
||||
richTexts.any((rt) => rt.text.toPlainText().contains('example'));
|
||||
expect(hasExampleText, isTrue);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user