Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 27 additions & 6 deletions lib/features/main_screen/results_tab.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import 'dart:async' show unawaited;

import 'package:flutter/material.dart' as material;
import 'package:querya_desktop/core/motion/querya_fade_slide.dart';
import 'package:querya_desktop/core/widgets/virtual_selectable_text_view.dart';
import 'package:querya_desktop/features/main_screen/result_grid_view.dart';
import 'package:querya_desktop/shared/services/data_export_service.dart';
import 'package:querya_desktop/shared/widgets/widgets.dart';

/// Query output: grid, loading, error, or placeholder.
///
/// Mode changes (idle / loading / error / status / grid) morph via
/// [QueryaFadeSlide]. Keys are per **mode**, not per row — so grid data updates
/// and scroll rebuilds do not re-trigger the transition.
class ResultsTab extends StatelessWidget {
const ResultsTab({
super.key,
Expand All @@ -29,24 +34,37 @@ class ResultsTab extends StatelessWidget {

@override
Widget build(BuildContext context) {
return QueryaFadeSlide(
alignment: material.Alignment.center,
offset: const material.Offset(0, 0.015),
child: _buildBody(context),
);
}

material.Widget _buildBody(material.BuildContext context) {
if (isLoading) {
return const material.Center(
key: material.ValueKey('results_mode_loading'),
child: material.CircularProgressIndicator(),
);
}
if (errorMessage != null && errorMessage!.isNotEmpty) {
return VirtualSelectableTextView(
text: errorMessage!,
style: material.TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color: Theme.of(context).colorScheme.destructive,
return material.KeyedSubtree(
key: const material.ValueKey('results_mode_error'),
child: VirtualSelectableTextView(
text: errorMessage!,
style: material.TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color: Theme.of(context).colorScheme.destructive,
),
),
);
}
if (columns.isEmpty && rows.isEmpty) {
if (statusLine != null) {
return material.Padding(
key: const material.ValueKey('results_mode_status'),
padding: const material.EdgeInsets.all(16),
child: Align(
alignment: material.Alignment.topLeft,
Expand All @@ -56,15 +74,18 @@ class ResultsTab extends StatelessWidget {
}
if (affectedRows != null) {
return material.Center(
key: const material.ValueKey('results_mode_affected'),
child: Text('Rows affected: $affectedRows').muted(),
);
}
return material.Center(
key: const material.ValueKey('results_mode_idle'),
child: const Text('Run a query to see results here.').muted(),
);
}

return material.Column(
key: const material.ValueKey('results_mode_grid'),
crossAxisAlignment: material.CrossAxisAlignment.stretch,
children: [
if (showExportToolbar && columns.isNotEmpty)
Expand Down
244 changes: 240 additions & 4 deletions test/features/main_screen/results_tab_test.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import 'package:flutter/material.dart' as material;
import 'package:flutter_test/flutter_test.dart';
import 'package:querya_desktop/core/motion/querya_fade_slide.dart';
import 'package:querya_desktop/core/motion/querya_motion_scope.dart';
import 'package:querya_desktop/features/main_screen/result_grid_view.dart';
import 'package:querya_desktop/features/main_screen/results_tab.dart';

import '../../support/querya_theme_test_shell.dart';

void main() {
material.Widget resultsShell({
required material.Widget child,
QueryaMotionLevel level = QueryaMotionLevel.full,
}) {
return queryaThemeTestShell(
child: QueryaMotionScope(
level: level,
child: child,
),
);
}

group('computeResultGridColumnWidths', () {
test('returns empty list for no columns', () {
expect(
Expand Down Expand Up @@ -55,7 +69,7 @@ void main() {
);

await tester.pumpWidget(
queryaThemeTestShell(
resultsShell(
child: material.Scaffold(
body: ResultsTab(
columns: const ['id', 'name'],
Expand All @@ -69,6 +83,11 @@ void main() {
expect(find.byType(material.ListView), findsOneWidget);
expect(find.byType(material.Table), findsNothing);
expect(find.byType(VirtualResultGrid), findsOneWidget);
expect(find.byType(QueryaFadeSlide), findsOneWidget);
expect(
find.byKey(const material.ValueKey('results_mode_grid')),
findsOneWidget,
);
});

testWidgets('virtualizes rows — does not build all row widgets at once',
Expand All @@ -79,7 +98,7 @@ void main() {
);

await tester.pumpWidget(
queryaThemeTestShell(
resultsShell(
child: material.SizedBox(
height: 400,
width: 600,
Expand All @@ -102,7 +121,7 @@ void main() {
'recalculates column widths when updated with different columns without throwing RangeError',
(tester) async {
await tester.pumpWidget(
queryaThemeTestShell(
resultsShell(
child: const material.SizedBox(
height: 400,
width: 600,
Expand All @@ -119,7 +138,7 @@ void main() {

// Now update the grid with 5 columns instead of 2
await tester.pumpWidget(
queryaThemeTestShell(
resultsShell(
child: const material.SizedBox(
height: 400,
width: 600,
Expand All @@ -138,5 +157,222 @@ void main() {
expect(find.text('email'), findsOneWidget);
expect(find.text('created_at'), findsOneWidget);
});

testWidgets('shows idle / loading / error / grid mode keys', (tester) async {
await tester.pumpWidget(
resultsShell(
child: const material.Scaffold(
body: ResultsTab(),
),
),
);
await tester.pump();
expect(find.text('Run a query to see results here.'), findsOneWidget);
expect(
find.byKey(const material.ValueKey('results_mode_idle')),
findsOneWidget,
);

await tester.pumpWidget(
resultsShell(
child: const material.Scaffold(
body: ResultsTab(isLoading: true),
),
),
);
await tester.pump();
expect(
find.byKey(const material.ValueKey('results_mode_loading')),
findsOneWidget,
);
expect(find.byType(material.CircularProgressIndicator), findsOneWidget);

await tester.pumpWidget(
resultsShell(
child: const material.Scaffold(
body: ResultsTab(errorMessage: 'syntax error near SELECT'),
),
),
);
await tester.pumpAndSettle();
expect(
find.byKey(const material.ValueKey('results_mode_error')),
findsOneWidget,
);
expect(find.textContaining('syntax error'), findsOneWidget);

await tester.pumpWidget(
resultsShell(
child: const material.Scaffold(
body: ResultsTab(
columns: ['id'],
rows: [
['1'],
],
),
),
),
);
await tester.pumpAndSettle();
expect(
find.byKey(const material.ValueKey('results_mode_grid')),
findsOneWidget,
);
});

testWidgets('morphs idle → loading → grid through FadeSlide', (tester) async {
await tester.pumpWidget(
resultsShell(
child: const material.Scaffold(
body: ResultsTab(),
),
),
);
await tester.pump();
expect(
find.byKey(const material.ValueKey('results_mode_idle')),
findsOneWidget,
);

await tester.pumpWidget(
resultsShell(
child: const material.Scaffold(
body: ResultsTab(isLoading: true),
),
),
);
await tester.pump(const Duration(milliseconds: 40));
expect(
find.byKey(const material.ValueKey('results_mode_loading')),
findsOneWidget,
);

await tester.pumpWidget(
resultsShell(
child: const material.Scaffold(
body: ResultsTab(
columns: ['id', 'name'],
rows: [
['1', 'alpha'],
],
),
),
),
);
await tester.pumpAndSettle();
expect(find.byType(VirtualResultGrid), findsOneWidget);
expect(find.text('alpha'), findsOneWidget);
expect(
find.byKey(const material.ValueKey('results_mode_idle')),
findsNothing,
);
expect(
find.byKey(const material.ValueKey('results_mode_loading')),
findsNothing,
);
});

testWidgets('motion off snaps modes without lingering previous key',
(tester) async {
await tester.pumpWidget(
resultsShell(
level: QueryaMotionLevel.off,
child: const material.Scaffold(
body: ResultsTab(isLoading: true),
),
),
);
await tester.pump();

await tester.pumpWidget(
resultsShell(
level: QueryaMotionLevel.off,
child: const material.Scaffold(
body: ResultsTab(errorMessage: 'boom'),
),
),
);
await tester.pump();
expect(
find.byKey(const material.ValueKey('results_mode_loading')),
findsNothing,
);
expect(
find.byKey(const material.ValueKey('results_mode_error')),
findsOneWidget,
);
});

testWidgets('grid row updates do not change mode key', (tester) async {
await tester.pumpWidget(
resultsShell(
child: const material.Scaffold(
body: ResultsTab(
columns: ['id'],
rows: [
['1'],
],
),
),
),
);
await tester.pumpAndSettle();
expect(
find.byKey(const material.ValueKey('results_mode_grid')),
findsOneWidget,
);

await tester.pumpWidget(
resultsShell(
child: const material.Scaffold(
body: ResultsTab(
columns: ['id'],
rows: [
['1'],
['2'],
['3'],
],
),
),
),
);
await tester.pump();
// Same mode key — no second grid body from a mode switch.
expect(
find.byKey(const material.ValueKey('results_mode_grid')),
findsOneWidget,
);
expect(find.text('3'), findsOneWidget);
});

testWidgets('shows status and affected empty modes', (tester) async {
await tester.pumpWidget(
resultsShell(
child: const material.Scaffold(
body: ResultsTab(statusLine: 'Connected to db'),
),
),
);
await tester.pump();
expect(
find.byKey(const material.ValueKey('results_mode_status')),
findsOneWidget,
);
expect(find.text('Connected to db'), findsOneWidget);

await tester.pumpWidget(
resultsShell(
child: const material.Scaffold(
body: ResultsTab(affectedRows: 4),
),
),
);
await tester.pumpAndSettle();
expect(
find.byKey(const material.ValueKey('results_mode_affected')),
findsOneWidget,
);
expect(find.text('Rows affected: 4'), findsOneWidget);
});
});
}
Loading