oto/packages/flutter/oto_console/test/oto_console_test.dart
toki 9e844ba217 feat: update agent-ui definitions and oto_console components
- Refresh agent-ui definition files for console-shell, console-rail
- Update oto_console_shell.dart and oto_jobs_surface.dart
- Refresh widget tests and ot_console_test.dart
- Archive G07 shell jobs review and plan
2026-07-03 23:15:14 +09:00

869 lines
26 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);
// Jenkins IA shell: breadcrumb and Dashboard-level left menu labels.
expect(find.text('OTO'), findsOneWidget);
expect(find.text('New Item'), findsOneWidget);
expect(find.text('Pipelines'), findsOneWidget);
expect(find.text('Build History'), 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('OtoRunnersSurface renders runner action states', (tester) async {
final runners = [
const OtoRunnerViewModel(
runnerID: 'runner-1',
alias: 'Test Runner',
status: 'idle',
currentJobID: '',
currentExecutionID: '',
message: '',
),
];
OtoRunnerSelfUpdateDraft? capturedDraft;
// --- Idle state: form visible, submit disabled (empty fields) ---
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: OtoRunnersSurface(
snapshot: OtoSurfaceSnapshot.data(runners),
onSelfUpdateRunner: (draft) {
capturedDraft = draft;
},
),
),
),
);
expect(find.text('Self Update'), findsOneWidget);
expect(find.text('Update'), findsOneWidget);
final disabledBtn = tester.widget<ElevatedButton>(
find.ancestor(
of: find.text('Update'),
matching: find.byType(ElevatedButton),
),
);
expect(disabledBtn.onPressed, isNull);
// --- HTTPS URL validation: non-HTTPS keeps button disabled ---
await tester.enterText(find.byType(TextField).at(0), 'v1.2.3');
await tester.pump();
await tester.enterText(
find.byType(TextField).at(1),
'http://not-https.example.com/oto',
);
await tester.pump();
final httpBtn = tester.widget<ElevatedButton>(
find.ancestor(
of: find.text('Update'),
matching: find.byType(ElevatedButton),
),
);
expect(httpBtn.onPressed, isNull);
// --- HTTPS URL: button enabled ---
await tester.enterText(
find.byType(TextField).at(1),
'https://releases.example.test/oto-agent',
);
await tester.pump();
final enabledBtn = tester.widget<ElevatedButton>(
find.ancestor(
of: find.text('Update'),
matching: find.byType(ElevatedButton),
),
);
expect(enabledBtn.onPressed, isNotNull);
// --- Tap submit: callback receives correct draft ---
await tester.tap(find.text('Update'));
await tester.pump();
expect(capturedDraft, isNotNull);
expect(capturedDraft!.runnerID, 'runner-1');
expect(capturedDraft!.version, 'v1.2.3');
expect(
capturedDraft!.downloadUrl,
'https://releases.example.test/oto-agent',
);
// --- Submitting state: spinner shown, button disabled ---
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: OtoRunnersSurface(
snapshot: OtoSurfaceSnapshot.data(runners),
actionStates: {'runner-1': const OtoActionViewState.submitting()},
onSelfUpdateRunner: (draft) {},
),
),
),
);
expect(find.byType(CircularProgressIndicator), findsAtLeast(1));
final submittingBtn = tester.widget<ElevatedButton>(
find.ancestor(
of: find.byType(CircularProgressIndicator).first,
matching: find.byType(ElevatedButton),
),
);
expect(submittingBtn.onPressed, isNull);
// --- Deferred state: deferred message shown ---
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: OtoRunnersSurface(
snapshot: OtoSurfaceSnapshot.data(runners),
actionStates: {
'runner-1': const OtoActionViewState.deferred(
message: 'Update scheduled',
),
},
onSelfUpdateRunner: (draft) {},
),
),
),
);
expect(find.textContaining('Deferred: Update scheduled'), findsOneWidget);
// --- Error state: error message shown ---
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: OtoRunnersSurface(
snapshot: OtoSurfaceSnapshot.data(runners),
actionStates: {
'runner-1': const OtoActionViewState.failed(
message: 'Runner not found',
),
},
onSelfUpdateRunner: (draft) {},
),
),
),
);
expect(find.textContaining('Runner not found'), 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);
// Jenkins dashboard job table: header columns and job-context task menu.
expect(find.text('Name'), findsOneWidget);
expect(find.text('Last Run'), findsOneWidget);
expect(find.text('Updated'), findsOneWidget);
expect(find.text('Actions'), findsOneWidget);
// 'Status' appears as both a table header and a job-context task menu
// chip; 'Build History' appears in the context strip and the task menu.
expect(find.text('Status'), findsNWidgets(2));
expect(find.text('Build History'), findsNWidgets(2));
expect(find.text('Stages'), findsOneWidget);
expect(find.text('Build Queue'), 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);
// No run selected yet: detail region shows the placeholder.
expect(find.text('Select a run to view details.'), 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),
),
),
),
);
// Selected run detail: run header + stage summary placeholder + panes.
expect(find.text('Run exec-1'), findsOneWidget);
expect(find.text('Stage summary unavailable'), findsOneWidget);
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);
});
testWidgets('OtoExecutionsSurface renders execution action states', (
tester,
) async {
final executionsWithRunner = [
const OtoExecutionViewModel(
executionID: 'exec-1',
jobID: 'job-1',
state: 'running',
runnerID: 'runner-1',
createdAt: '2026-06-15T00:01:00Z',
updatedAt: '2026-06-15T00:05:00Z',
),
];
String? capturedReportExecID;
bool capturedReportSuccess = false;
String? capturedLogExecID;
String? capturedArtifactExecID;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: OtoExecutionsSurface(
snapshot: OtoSurfaceSnapshot.data(executionsWithRunner),
expandedExecutionID: 'exec-1',
onReportExecution: (draft) {
capturedReportExecID = draft.executionID;
capturedReportSuccess = draft.success;
},
onAppendLog: (draft) {
capturedLogExecID = draft.executionID;
},
onAppendArtifact: (draft) {
capturedArtifactExecID = draft.executionID;
},
),
),
),
);
expect(find.text('Actions'), findsOneWidget);
expect(find.text('Cancel'), findsOneWidget);
expect(find.text('Report'), findsOneWidget);
expect(find.text('Log'), findsOneWidget);
expect(find.text('Artifact'), findsOneWidget);
// Test Report callback
await tester.tap(find.text('Report'));
await tester.pumpAndSettle();
expect(capturedReportExecID, 'exec-1');
expect(capturedReportSuccess, true);
// Test Log callback
capturedLogExecID = null;
await tester.tap(find.text('Log'));
await tester.pumpAndSettle();
expect(capturedLogExecID, 'exec-1');
// Test Artifact callback
capturedArtifactExecID = null;
await tester.tap(find.text('Artifact'));
await tester.pumpAndSettle();
expect(capturedArtifactExecID, 'exec-1');
// --- Submitting state shows spinner ---
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: OtoExecutionsSurface(
snapshot: OtoSurfaceSnapshot.data(executionsWithRunner),
expandedExecutionID: 'exec-1',
actionStates: {'exec-1': const OtoActionViewState.submitting()},
onReportExecution: (draft) {},
onAppendLog: (draft) {},
onAppendArtifact: (draft) {},
),
),
),
);
expect(find.byType(CircularProgressIndicator), findsAtLeast(1));
// --- Success state shows feedback text ---
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: OtoExecutionsSurface(
snapshot: OtoSurfaceSnapshot.data(executionsWithRunner),
expandedExecutionID: 'exec-1',
actionStates: {
'exec-1': const OtoActionViewState.succeeded(
message: 'Report accepted',
),
},
onReportExecution: (draft) {},
onAppendLog: (draft) {},
onAppendArtifact: (draft) {},
),
),
),
);
expect(find.textContaining('Report accepted'), findsOneWidget);
// --- Failed state shows error text ---
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: OtoExecutionsSurface(
snapshot: OtoSurfaceSnapshot.data(executionsWithRunner),
expandedExecutionID: 'exec-1',
actionStates: {
'exec-1': const OtoActionViewState.failed(
message: 'Cancel rejected',
),
},
onReportExecution: (draft) {},
onAppendLog: (draft) {},
onAppendArtifact: (draft) {},
),
),
),
);
expect(find.textContaining('Cancel rejected'), findsOneWidget);
});
testWidgets('OtoExecutionsSurface shows Actions section when expanded', (
tester,
) async {
final executionsWithRunner = [
const OtoExecutionViewModel(
executionID: 'exec-actions',
jobID: 'job-1',
state: 'running',
runnerID: 'runner-1',
createdAt: '2026-06-15T00:01:00Z',
updatedAt: '2026-06-15T00:05:00Z',
),
];
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: OtoExecutionsSurface(
snapshot: OtoSurfaceSnapshot.data(executionsWithRunner),
expandedExecutionID: 'exec-actions',
),
),
),
);
expect(find.text('Actions'), findsOneWidget);
expect(find.byIcon(Icons.stop_circle), findsOneWidget);
expect(find.byIcon(Icons.check_circle), findsOneWidget);
expect(find.byIcon(Icons.bug_report), findsOneWidget);
expect(find.byIcon(Icons.insert_drive_file), findsOneWidget);
});
testWidgets('OtoJobsSurface renders job create action states', (
tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: OtoJobsSurface(
snapshot: const OtoSurfaceSnapshot.empty(),
jobCreateState: const OtoActionViewState.idle(),
onCreateJob: (draft) {},
),
),
),
);
expect(find.textContaining('New Item'), findsOneWidget);
expect(find.byType(TextField), findsNWidgets(2));
final createButton = find.ancestor(
of: find.text('Create'),
matching: find.byType(ElevatedButton),
);
expect(createButton, findsOneWidget);
final disabledButton = tester.widget<ElevatedButton>(createButton);
expect(disabledButton.onPressed, isNull);
await tester.enterText(find.byType(TextField).at(0), 'my-job-id');
await tester.pump();
var button = tester.widget<ElevatedButton>(
find.ancestor(
of: find.text('Create'),
matching: find.byType(ElevatedButton),
),
);
expect(button.onPressed, isNull);
await tester.enterText(find.byType(TextField).at(1), 'My Job Name');
await tester.pump();
button = tester.widget<ElevatedButton>(
find.ancestor(
of: find.text('Create'),
matching: find.byType(ElevatedButton),
),
);
expect(button.onPressed, isNotNull);
OtoJobCreateDraft? capturedDraft;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: OtoJobsSurface(
snapshot: const OtoSurfaceSnapshot.empty(),
jobCreateState: const OtoActionViewState.idle(),
onCreateJob: (draft) {
capturedDraft = draft;
},
),
),
),
);
await tester.enterText(find.byType(TextField).at(0), 'draft-id');
await tester.pump();
await tester.enterText(find.byType(TextField).at(1), 'Draft Name');
await tester.pump();
await tester.tap(find.text('Create'));
await tester.pump();
expect(capturedDraft, isNotNull);
expect(capturedDraft!.id, 'draft-id');
expect(capturedDraft!.name, 'Draft Name');
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: OtoJobsSurface(
snapshot: const OtoSurfaceSnapshot.empty(),
jobCreateState: const OtoActionViewState.submitting(),
onCreateJob: (draft) {},
),
),
),
);
expect(find.byType(CircularProgressIndicator), findsOneWidget);
expect(find.byType(ElevatedButton), findsOneWidget);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: OtoJobsSurface(
snapshot: const OtoSurfaceSnapshot.empty(),
jobCreateState: const OtoActionViewState.succeeded(
message: 'Job created: job-123',
),
onCreateJob: (draft) {},
),
),
),
);
expect(find.textContaining('Job created'), findsOneWidget);
expect(find.textContaining('job-123'), findsOneWidget);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: OtoJobsSurface(
snapshot: const OtoSurfaceSnapshot.empty(),
jobCreateState: const OtoActionViewState.failed(
message: 'Invalid request',
),
onCreateJob: (draft) {},
),
),
),
);
expect(find.textContaining('Invalid request'), findsOneWidget);
});
}