mirror of
https://github.com/foss42/apidash.git
synced 2025-12-01 18:28:25 +08:00
231 lines
7.9 KiB
Dart
231 lines
7.9 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:apidash/providers/terminal_providers.dart';
|
|
import 'package:apidash/terminal/terminal.dart';
|
|
import 'package:apidash_core/apidash_core.dart';
|
|
|
|
void main() {
|
|
group('TerminalController', () {
|
|
late ProviderContainer container;
|
|
late TerminalController controller;
|
|
|
|
setUp(() {
|
|
container = ProviderContainer();
|
|
controller = container.read(terminalStateProvider.notifier);
|
|
});
|
|
|
|
tearDown(() {
|
|
container.dispose();
|
|
});
|
|
|
|
test('initial state is empty', () {
|
|
final state = container.read(terminalStateProvider);
|
|
expect(state.entries, isEmpty);
|
|
expect(state.index, isEmpty);
|
|
});
|
|
|
|
test('log system entries and clear', () {
|
|
controller.logSystem(category: 'ui', message: 'opened');
|
|
controller.logSystem(
|
|
category: 'provider', message: 'updated', level: TerminalLevel.warn);
|
|
final state = container.read(terminalStateProvider);
|
|
expect(state.entries.length, 2);
|
|
expect(state.entries.first.system?.category, anyOf('ui', 'provider'));
|
|
|
|
// serialization has timestamps, uppercase level and title-cased source
|
|
final text = controller.serializeAll();
|
|
expect(text, contains('INFO'));
|
|
expect(text, contains('System'));
|
|
expect(text.toLowerCase(), contains('opened'));
|
|
|
|
controller.clear();
|
|
expect(container.read(terminalStateProvider).entries, isEmpty);
|
|
});
|
|
|
|
test('JS logs map levels and include args', () {
|
|
controller.logJs(level: 'log', args: ['hello']);
|
|
controller.logJs(level: 'warn', args: ['warn']);
|
|
controller.logJs(level: 'error', args: ['err'], context: 'preRequest');
|
|
final state = container.read(terminalStateProvider);
|
|
expect(state.entries.length, 3);
|
|
final levels = state.entries.map((e) => e.level).toList();
|
|
expect(levels.contains(TerminalLevel.info), isTrue);
|
|
expect(levels.contains(TerminalLevel.warn), isTrue);
|
|
expect(levels.contains(TerminalLevel.error), isTrue);
|
|
|
|
final title0 = controller.titleFor(state.entries.first);
|
|
final sub0 = controller.subtitleFor(state.entries.first);
|
|
expect(title0.toLowerCase(), contains('js'));
|
|
expect(sub0, isNotNull);
|
|
});
|
|
|
|
test('network lifecycle: start -> chunk -> complete', () async {
|
|
final id = controller.startNetwork(
|
|
apiType: APIType.rest,
|
|
method: HTTPVerb.get,
|
|
url: 'https://example.com',
|
|
requestHeaders: const {'A': '1'},
|
|
requestBodyPreview: 'req',
|
|
isStreaming: true,
|
|
);
|
|
expect(container.read(terminalStateProvider).entries, isNotEmpty);
|
|
|
|
controller.addNetworkChunk(
|
|
id,
|
|
BodyChunk(
|
|
ts: DateTime.now(),
|
|
text: 'chunk1',
|
|
sizeBytes: 6,
|
|
),
|
|
);
|
|
|
|
controller.completeNetwork(
|
|
id,
|
|
statusCode: 200,
|
|
responseHeaders: const {'B': '2'},
|
|
responseBodyPreview: 'ok',
|
|
duration: const Duration(milliseconds: 88),
|
|
);
|
|
|
|
final e = container.read(terminalStateProvider).entries.first;
|
|
expect(e.network?.phase, NetworkPhase.completed);
|
|
expect(e.level, TerminalLevel.info);
|
|
expect(e.network?.responseStatus, 200);
|
|
expect(e.network?.duration?.inMilliseconds, 88);
|
|
|
|
// Helpers
|
|
final title = controller.titleFor(e);
|
|
final sub = controller.subtitleFor(e);
|
|
expect(title, contains('GET'));
|
|
expect(title, contains('https://example.com'));
|
|
expect(sub, 'ok');
|
|
|
|
// Serialization should include status code
|
|
final ser = controller.serializeAll();
|
|
expect(ser, contains('200'));
|
|
});
|
|
|
|
test('network failure switches level to error', () {
|
|
final id = controller.startNetwork(
|
|
apiType: APIType.rest,
|
|
method: HTTPVerb.post,
|
|
url: 'https://api',
|
|
);
|
|
controller.failNetwork(id, 'timeout');
|
|
final e = container.read(terminalStateProvider).entries.first;
|
|
expect(e.level, TerminalLevel.error);
|
|
expect(e.network?.phase, NetworkPhase.failed);
|
|
expect(controller.subtitleFor(e), 'timeout');
|
|
});
|
|
|
|
test('completeNetwork maps 4xx/5xx to error level', () {
|
|
final id = controller.startNetwork(
|
|
apiType: APIType.rest,
|
|
method: HTTPVerb.get,
|
|
url: 'https://api',
|
|
);
|
|
controller.completeNetwork(id,
|
|
statusCode: 404, responseBodyPreview: 'nf');
|
|
final e = container.read(terminalStateProvider).entries.first;
|
|
expect(e.level, TerminalLevel.error);
|
|
expect(e.network?.responseStatus, 404);
|
|
expect(controller.subtitleFor(e), 'nf');
|
|
});
|
|
|
|
test('progress chunks update phase and accumulate data', () {
|
|
final id = controller.startNetwork(
|
|
apiType: APIType.rest,
|
|
method: HTTPVerb.get,
|
|
url: 'https://chunks',
|
|
isStreaming: true,
|
|
);
|
|
controller.addNetworkChunk(
|
|
id,
|
|
BodyChunk(ts: DateTime.now(), text: 'part1', sizeBytes: 5),
|
|
);
|
|
controller.addNetworkChunk(
|
|
id,
|
|
BodyChunk(ts: DateTime.now(), text: 'part2', sizeBytes: 5),
|
|
);
|
|
final e = container.read(terminalStateProvider).entries.first;
|
|
expect(e.network?.phase, NetworkPhase.progress);
|
|
expect(e.network?.chunks.length, 2);
|
|
expect(e.network?.chunks.first.text, 'part1');
|
|
});
|
|
|
|
test('entries are stored newest first and index maps ids', () {
|
|
controller.logSystem(category: 'a', message: 'm1');
|
|
controller.logSystem(category: 'b', message: 'm2');
|
|
final entries = container.read(terminalStateProvider).entries;
|
|
// Last logged appears first
|
|
expect(entries.first.system?.category, anyOf('a', 'b'));
|
|
final idxMap = container.read(terminalStateProvider).index;
|
|
expect(idxMap.containsKey(entries.first.id), isTrue);
|
|
expect(idxMap[entries.first.id], 0);
|
|
});
|
|
|
|
test('serializeAll with provided entries includes ISO timestamps', () {
|
|
final e1 = TerminalEntry(
|
|
id: 'x1',
|
|
ts: DateTime.fromMillisecondsSinceEpoch(0, isUtc: true),
|
|
source: TerminalSource.system,
|
|
level: TerminalLevel.info,
|
|
system: SystemLogData(category: 'ui', message: 'hello'),
|
|
);
|
|
final e2 = TerminalEntry(
|
|
id: 'x2',
|
|
ts: DateTime.fromMillisecondsSinceEpoch(1000, isUtc: true),
|
|
source: TerminalSource.js,
|
|
level: TerminalLevel.error,
|
|
js: JsLogData(level: 'error', args: ['boom']),
|
|
);
|
|
final text = controller.serializeAll(entries: [e1, e2]);
|
|
expect(text, contains('[1970-01-01T00:00:00.000Z]'));
|
|
expect(text, contains('[1970-01-01T00:00:01.000Z]'));
|
|
expect(text, contains('System'));
|
|
expect(text, contains('JS error'));
|
|
});
|
|
});
|
|
|
|
group('TerminalState.copyWith', () {
|
|
test('returns same entries when no new entries provided', () {
|
|
final e1 = TerminalEntry(
|
|
id: 'id1',
|
|
source: TerminalSource.system,
|
|
level: TerminalLevel.info,
|
|
system: SystemLogData(category: 'ui', message: 'hello'),
|
|
);
|
|
final s1 = TerminalState(entries: [e1]);
|
|
final s2 = s1.copyWith();
|
|
|
|
expect(identical(s1.entries, s2.entries), isTrue);
|
|
expect(s2.index[e1.id], 0);
|
|
expect(s2.index.length, 1);
|
|
});
|
|
|
|
test('rebuilds index when entries list changes', () {
|
|
final e1 = TerminalEntry(
|
|
id: 'id1',
|
|
source: TerminalSource.system,
|
|
level: TerminalLevel.info,
|
|
system: SystemLogData(category: 'ui', message: 'hello'),
|
|
);
|
|
final e2 = TerminalEntry(
|
|
id: 'id2',
|
|
source: TerminalSource.js,
|
|
level: TerminalLevel.error,
|
|
js: JsLogData(level: 'error', args: ['boom']),
|
|
);
|
|
|
|
final s1 = TerminalState(entries: [e1]);
|
|
final s2 = s1.copyWith(entries: [e2, e1]);
|
|
|
|
expect(identical(s1.entries, s2.entries), isFalse);
|
|
expect(s2.entries, orderedEquals([e2, e1]));
|
|
expect(s2.index[e2.id], 0);
|
|
expect(s2.index[e1.id], 1);
|
|
expect(s2.index.length, 2);
|
|
});
|
|
});
|
|
}
|