- execution surface 및 preview smoke 테스트 문서 업데이트 - client 앱 및 oto_console UI surface (artifacts, executions, jobs) 추가 - 관련 테스트 및 계약 문서 업데이트 - archive 구조 정리
362 lines
12 KiB
Dart
362 lines
12 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:oto_console/oto_console.dart';
|
|
|
|
void main() {
|
|
const onlineConnection = OtoCoreConnectionSnapshot(
|
|
state: OtoCoreConnectionState.online,
|
|
health: OtoCoreEndpointStatus(
|
|
label: 'Health',
|
|
path: '/healthz',
|
|
state: OtoCoreConnectionState.online,
|
|
statusCode: 200,
|
|
message: 'OK',
|
|
),
|
|
readiness: OtoCoreEndpointStatus(
|
|
label: 'Readiness',
|
|
path: '/readyz',
|
|
state: OtoCoreConnectionState.online,
|
|
statusCode: 200,
|
|
message: 'OK',
|
|
),
|
|
policyNote: 'CORS headers or a same-origin proxy are required.',
|
|
);
|
|
|
|
test('exports console contract models without shell dependency', () {
|
|
expect(
|
|
OtoConsoleSection.values.map((section) => section.name),
|
|
containsAllInOrder([
|
|
'overview',
|
|
'runners',
|
|
'pipelines',
|
|
'executions',
|
|
'artifacts',
|
|
'agent',
|
|
'settings',
|
|
]),
|
|
);
|
|
|
|
final contractFile = File('lib/src/oto_console_contract.dart');
|
|
final contractSource =
|
|
(contractFile.existsSync()
|
|
? contractFile
|
|
: File(
|
|
'packages/flutter/oto_console/lib/src/oto_console_contract.dart',
|
|
))
|
|
.readAsStringSync();
|
|
expect(contractSource, isNot(contains('oto_console_shell.dart')));
|
|
});
|
|
|
|
testWidgets('renders embeddable OTO console shell', (tester) async {
|
|
const config = OtoConsoleConfig(
|
|
serverHttpUrl: 'http://localhost:8080',
|
|
serverWireUrl: 'ws://localhost:18080/runner',
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
const MaterialApp(
|
|
home: OtoConsoleShell(
|
|
config: config,
|
|
capabilities: OtoCapabilityPack(
|
|
capabilities: [
|
|
OtoCapability(
|
|
name: 'Remote Run',
|
|
description: 'Dispatch remote build runs.',
|
|
),
|
|
],
|
|
),
|
|
overview: OtoConsoleOverview(
|
|
config: config,
|
|
connection: onlineConnection,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.text('Build Operations Overview'), findsOneWidget);
|
|
expect(find.text('CORE ONLINE'), findsOneWidget);
|
|
expect(find.text('Health'), findsOneWidget);
|
|
expect(find.text('Readiness'), findsOneWidget);
|
|
expect(find.byTooltip('Agent'), findsOneWidget);
|
|
|
|
await tester.tap(find.byTooltip('Agent'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.textContaining('OTO agent surface is ready'), findsOneWidget);
|
|
expect(find.textContaining('Remote Run'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('renders section surfaces with empty states', (tester) async {
|
|
const config = OtoConsoleConfig(
|
|
serverHttpUrl: 'http://localhost:8080',
|
|
serverWireUrl: 'ws://localhost:18080/runner',
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
const MaterialApp(
|
|
home: OtoConsoleShell(
|
|
config: config,
|
|
overview: OtoConsoleOverview(
|
|
config: config,
|
|
connection: onlineConnection,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final sections = <String, String>{
|
|
'Runners': 'No runners connected',
|
|
'Pipelines': 'No pipeline jobs',
|
|
'Executions': 'No executions',
|
|
'Artifacts': 'No artifacts',
|
|
'Settings': 'No local preferences',
|
|
};
|
|
|
|
for (final entry in sections.entries) {
|
|
await tester.tap(find.byTooltip(entry.key));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text(entry.value), findsOneWidget);
|
|
}
|
|
|
|
await tester.tap(find.byTooltip('Settings'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Browser API policy'), findsOneWidget);
|
|
expect(find.textContaining('same-origin proxy'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('OtoRunnersSurface renders loading state', (tester) async {
|
|
await tester.pumpWidget(
|
|
const MaterialApp(
|
|
home: Scaffold(
|
|
body: OtoRunnersSurface(snapshot: OtoSurfaceSnapshot.loading()),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('OtoRunnersSurface renders empty state', (tester) async {
|
|
await tester.pumpWidget(
|
|
const MaterialApp(
|
|
home: Scaffold(
|
|
body: OtoRunnersSurface(snapshot: OtoSurfaceSnapshot.empty()),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.text('No runners connected'), findsOneWidget);
|
|
expect(find.text('Runner registry is empty.'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('OtoRunnersSurface renders error state', (tester) async {
|
|
await tester.pumpWidget(
|
|
const MaterialApp(
|
|
home: Scaffold(
|
|
body: OtoRunnersSurface(
|
|
snapshot: OtoSurfaceSnapshot.error('Failed to fetch runners'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.text('Error loading runners'), findsOneWidget);
|
|
expect(find.text('Failed to fetch runners'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('OtoRunnersSurface renders data state with runners list', (
|
|
tester,
|
|
) async {
|
|
final runners = [
|
|
const OtoRunnerViewModel(
|
|
runnerID: 'runner-1',
|
|
alias: 'Main Runner',
|
|
status: 'active',
|
|
currentJobID: 'job-101',
|
|
currentExecutionID: 'exec-202',
|
|
message: 'Running jobs',
|
|
),
|
|
const OtoRunnerViewModel(
|
|
runnerID: 'runner-2',
|
|
alias: 'Backup Runner',
|
|
status: 'offline',
|
|
currentJobID: '',
|
|
currentExecutionID: '',
|
|
message: 'Connection lost',
|
|
),
|
|
];
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: OtoRunnersSurface(snapshot: OtoSurfaceSnapshot.data(runners)),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.text('ID: runner-1'), findsOneWidget);
|
|
expect(find.text('Alias: Main Runner'), findsOneWidget);
|
|
expect(find.text('ACTIVE'), findsOneWidget);
|
|
expect(find.text('Message: Running jobs'), findsOneWidget);
|
|
expect(find.textContaining('Job: job-101'), findsOneWidget);
|
|
expect(find.textContaining('Execution: exec-202'), findsOneWidget);
|
|
|
|
expect(find.text('ID: runner-2'), findsOneWidget);
|
|
expect(find.text('Alias: Backup Runner'), findsOneWidget);
|
|
expect(find.text('OFFLINE'), findsOneWidget);
|
|
expect(find.text('Message: Connection lost'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('OtoJobsSurface renders loading/empty/error/data states', (tester) async {
|
|
// Loading
|
|
await tester.pumpWidget(const MaterialApp(home: Scaffold(body: OtoJobsSurface(snapshot: OtoSurfaceSnapshot.loading()))));
|
|
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
|
|
|
// Empty
|
|
await tester.pumpWidget(const MaterialApp(home: Scaffold(body: OtoJobsSurface(snapshot: OtoSurfaceSnapshot.empty()))));
|
|
expect(find.text('No pipeline jobs'), findsOneWidget);
|
|
expect(find.text('Job queue is empty.'), findsOneWidget);
|
|
|
|
// Error
|
|
await tester.pumpWidget(const MaterialApp(home: Scaffold(body: OtoJobsSurface(snapshot: OtoSurfaceSnapshot.error('Failed to fetch jobs')))));
|
|
expect(find.text('Error loading jobs'), findsOneWidget);
|
|
expect(find.text('Failed to fetch jobs'), findsOneWidget);
|
|
|
|
// Data
|
|
final jobs = [
|
|
const OtoJobViewModel(
|
|
jobID: 'job-1',
|
|
name: 'Build Job',
|
|
state: 'running',
|
|
executionID: 'exec-1',
|
|
createdAt: '2026-06-15T00:00:00Z',
|
|
updatedAt: '2026-06-15T00:05:00Z',
|
|
),
|
|
];
|
|
String? tappedExecution;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: OtoJobsSurface(
|
|
snapshot: OtoSurfaceSnapshot.data(jobs),
|
|
onExecutionTap: (id) => tappedExecution = id,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(find.text('Build Job'), findsOneWidget);
|
|
expect(find.text('RUNNING'), findsOneWidget);
|
|
expect(find.text('Job ID: job-1'), findsOneWidget);
|
|
expect(find.textContaining('Execution: exec-1'), findsOneWidget);
|
|
|
|
await tester.tap(find.textContaining('Execution: exec-1'));
|
|
await tester.pumpAndSettle();
|
|
expect(tappedExecution, 'exec-1');
|
|
});
|
|
|
|
testWidgets('OtoExecutionsSurface renders loading/empty/error/data states and expanded detail', (tester) async {
|
|
// Loading
|
|
await tester.pumpWidget(const MaterialApp(home: Scaffold(body: OtoExecutionsSurface(snapshot: OtoSurfaceSnapshot.loading()))));
|
|
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
|
|
|
// Empty
|
|
await tester.pumpWidget(const MaterialApp(home: Scaffold(body: OtoExecutionsSurface(snapshot: OtoSurfaceSnapshot.empty()))));
|
|
expect(find.text('No executions'), findsOneWidget);
|
|
expect(find.text('Execution history is empty.'), findsOneWidget);
|
|
|
|
// Error
|
|
await tester.pumpWidget(const MaterialApp(home: Scaffold(body: OtoExecutionsSurface(snapshot: OtoSurfaceSnapshot.error('Failed to fetch executions')))));
|
|
expect(find.text('Error loading executions'), findsOneWidget);
|
|
expect(find.text('Failed to fetch executions'), findsOneWidget);
|
|
|
|
// Data
|
|
final executions = [
|
|
const OtoExecutionViewModel(
|
|
executionID: 'exec-1',
|
|
jobID: 'job-1',
|
|
state: 'succeeded',
|
|
runnerID: 'runner-1',
|
|
createdAt: '2026-06-15T00:01:00Z',
|
|
updatedAt: '2026-06-15T00:05:00Z',
|
|
),
|
|
];
|
|
String? expandedId;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: OtoExecutionsSurface(
|
|
snapshot: OtoSurfaceSnapshot.data(executions),
|
|
onExpandExecution: (id) => expandedId = id,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(find.text('Exec ID: exec-1'), findsOneWidget);
|
|
expect(find.text('SUCCEEDED'), findsOneWidget);
|
|
expect(find.text('Job ID: job-1'), findsOneWidget);
|
|
expect(find.text('Runner ID: runner-1'), findsOneWidget);
|
|
|
|
await tester.tap(find.text('Exec ID: exec-1'));
|
|
await tester.pumpAndSettle();
|
|
expect(expandedId, 'exec-1');
|
|
|
|
// Expanded detail rendering
|
|
final logs = [
|
|
const OtoLogEntryViewModel(timestamp: '12:00:00', line: 'Starting build...'),
|
|
const OtoLogEntryViewModel(timestamp: '12:00:01', line: 'Finished successfully'),
|
|
];
|
|
final artifacts = [
|
|
const OtoArtifactViewModel(name: 'binary.exe', path: '/build/binary.exe'),
|
|
];
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: OtoExecutionsSurface(
|
|
snapshot: OtoSurfaceSnapshot.data(executions),
|
|
expandedExecutionID: 'exec-1',
|
|
logsSnapshot: OtoSurfaceSnapshot.data(logs),
|
|
artifactsSnapshot: OtoSurfaceSnapshot.data(artifacts),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(find.text('Logs Preview'), findsOneWidget);
|
|
expect(find.textContaining('Starting build...'), findsOneWidget);
|
|
expect(find.text('Artifacts'), findsOneWidget);
|
|
expect(find.text('binary.exe'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('OtoArtifactsSurface renders loading/empty/error/data states', (tester) async {
|
|
// Loading
|
|
await tester.pumpWidget(const MaterialApp(home: Scaffold(body: OtoArtifactsSurface(snapshot: OtoSurfaceSnapshot.loading()))));
|
|
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
|
|
|
// Empty
|
|
await tester.pumpWidget(const MaterialApp(home: Scaffold(body: OtoArtifactsSurface(snapshot: OtoSurfaceSnapshot.empty()))));
|
|
expect(find.text('No artifacts'), findsOneWidget);
|
|
expect(find.text('Artifact events are empty.'), findsOneWidget);
|
|
|
|
// Error
|
|
await tester.pumpWidget(const MaterialApp(home: Scaffold(body: OtoArtifactsSurface(snapshot: OtoSurfaceSnapshot.error('Failed to fetch artifacts')))));
|
|
expect(find.text('Error loading artifacts'), findsOneWidget);
|
|
expect(find.text('Failed to fetch artifacts'), findsOneWidget);
|
|
|
|
// Data
|
|
final artifacts = [
|
|
const OtoArtifactViewModel(name: 'app.apk', path: '/build/app.apk'),
|
|
];
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: OtoArtifactsSurface(snapshot: OtoSurfaceSnapshot.data(artifacts)),
|
|
),
|
|
),
|
|
);
|
|
expect(find.text('app.apk'), findsOneWidget);
|
|
expect(find.text('Path: /build/app.apk'), findsOneWidget);
|
|
});
|
|
}
|