feat(report): accomplishment report dialog + entry points
Add AccomplishmentReportDialog with programmer/admin modes, date-range filter (default this month), and include-unapproved toggle. Wire into DaySheetsTab app-bar action, ApprovalsTab app-bar action, and ProgrammerDrillDownScreen app-bar button. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -15,6 +15,7 @@ import '../../../widgets/app_status_summary.dart';
|
|||||||
import '../../../widgets/tech_chip.dart';
|
import '../../../widgets/tech_chip.dart';
|
||||||
import '../../reports/report_date_filter.dart';
|
import '../../reports/report_date_filter.dart';
|
||||||
import 'day_sheet_snapshot_builder.dart';
|
import 'day_sheet_snapshot_builder.dart';
|
||||||
|
import '../report/accomplishment_report_dialog.dart';
|
||||||
|
|
||||||
/// Admin tab for reviewing and bulk-approving programmer day sheets.
|
/// Admin tab for reviewing and bulk-approving programmer day sheets.
|
||||||
///
|
///
|
||||||
@@ -212,6 +213,16 @@ class _ApprovalsTabState extends ConsumerState<ApprovalsTab> {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.description_outlined),
|
||||||
|
tooltip: 'Accomplishment Report',
|
||||||
|
onPressed: () => showAccomplishmentReportDialog(
|
||||||
|
context,
|
||||||
|
ref,
|
||||||
|
programmerId: _programmerFilter,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import '../../../utils/programmer_task_ledger.dart';
|
|||||||
import '../../../widgets/app_state_view.dart';
|
import '../../../widgets/app_state_view.dart';
|
||||||
import '../../../widgets/app_status_summary.dart';
|
import '../../../widgets/app_status_summary.dart';
|
||||||
import '../../../widgets/status_pill.dart';
|
import '../../../widgets/status_pill.dart';
|
||||||
|
import '../report/accomplishment_report_dialog.dart';
|
||||||
|
|
||||||
/// Programmer-facing tab for managing personal day sheets.
|
/// Programmer-facing tab for managing personal day sheets.
|
||||||
///
|
///
|
||||||
@@ -140,6 +141,16 @@ class _DaySheetsTabState extends ConsumerState<DaySheetsTab> {
|
|||||||
return ListView(
|
return ListView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
children: [
|
children: [
|
||||||
|
// ── Report action ──────────────────────────────────────────────────
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.description_outlined),
|
||||||
|
tooltip: 'Accomplishment Report',
|
||||||
|
onPressed: () => showAccomplishmentReportDialog(context, ref),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// ── Today card ─────────────────────────────────────────────────────
|
// ── Today card ─────────────────────────────────────────────────────
|
||||||
_TodayCard(
|
_TodayCard(
|
||||||
total: todayTotal,
|
total: todayTotal,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import '../../widgets/app_section_header.dart';
|
|||||||
import '../../widgets/app_state_view.dart';
|
import '../../widgets/app_state_view.dart';
|
||||||
import '../../widgets/status_pill.dart';
|
import '../../widgets/status_pill.dart';
|
||||||
import 'dashboard/programmer_dashboard.dart';
|
import 'dashboard/programmer_dashboard.dart';
|
||||||
|
import 'report/accomplishment_report_dialog.dart';
|
||||||
|
|
||||||
/// Admin-only drill-down screen that shows [ProgrammerDashboard] for a
|
/// Admin-only drill-down screen that shows [ProgrammerDashboard] for a
|
||||||
/// specific user in read-only mode, plus their recent day sheets history.
|
/// specific user in read-only mode, plus their recent day sheets history.
|
||||||
@@ -52,7 +53,20 @@ class ProgrammerDrillDownScreen extends ConsumerWidget {
|
|||||||
final name = profile?.fullName ?? 'Programmer';
|
final name = profile?.fullName ?? 'Programmer';
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: Text(name)),
|
appBar: AppBar(
|
||||||
|
title: Text(name),
|
||||||
|
actions: [
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: () => showAccomplishmentReportDialog(
|
||||||
|
context,
|
||||||
|
ref,
|
||||||
|
programmerId: userId,
|
||||||
|
),
|
||||||
|
child: const Text('Generate Accomplishment Report'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -0,0 +1,282 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../../models/profile.model.dart';
|
||||||
|
import '../../../models/programmer_day_sheet.model.dart';
|
||||||
|
import '../../../providers/profile_provider.dart';
|
||||||
|
import '../../../providers/programmer_day_data_provider.dart';
|
||||||
|
import '../../../providers/programmer_day_sheets_provider.dart';
|
||||||
|
import '../../../providers/programmer_tasks_provider.dart';
|
||||||
|
import '../../../providers/reports_provider.dart';
|
||||||
|
import '../../../providers/supabase_provider.dart';
|
||||||
|
import '../../../utils/app_time.dart';
|
||||||
|
import '../../../utils/programmer_day_rows.dart';
|
||||||
|
import '../../../widgets/pdf_preview_dialog.dart';
|
||||||
|
import '../../reports/report_date_filter.dart';
|
||||||
|
import 'accomplishment_report_data.dart';
|
||||||
|
import 'accomplishment_report_pdf.dart';
|
||||||
|
|
||||||
|
/// Load accomplishment report data for [programmer] over [range].
|
||||||
|
///
|
||||||
|
/// For non-approved sheets when [includeUnapproved] is true, fetches live
|
||||||
|
/// rows via [fetchDayActivityData] + [buildDayRows]; approved sheets use
|
||||||
|
/// the stored snapshot.
|
||||||
|
Future<AccomplishmentReportData> loadAccomplishmentReport(
|
||||||
|
WidgetRef ref, {
|
||||||
|
required Profile programmer,
|
||||||
|
required ReportDateRange range,
|
||||||
|
required bool includeUnapproved,
|
||||||
|
}) async {
|
||||||
|
final sheets =
|
||||||
|
ref.read(daySheetsForUserProvider(programmer.id)).valueOrNull ?? [];
|
||||||
|
final profiles = ref.read(profilesProvider).valueOrNull ?? [];
|
||||||
|
final profileNames = {for (final p in profiles) p.id: p.fullName};
|
||||||
|
final tasks = ref.read(programmerTasksProvider).valueOrNull ?? [];
|
||||||
|
final client = ref.read(supabaseClientProvider);
|
||||||
|
|
||||||
|
final liveRowsByDay = <DateTime, List<DaySheetRow>>{};
|
||||||
|
if (includeUnapproved) {
|
||||||
|
for (final sheet in sheets) {
|
||||||
|
if (!sheet.workDate.isBefore(range.start) &&
|
||||||
|
sheet.workDate.isBefore(range.end) &&
|
||||||
|
sheet.status != DaySheetStatus.approved) {
|
||||||
|
final data = await fetchDayActivityData(
|
||||||
|
client,
|
||||||
|
day: sheet.workDate,
|
||||||
|
tasks: tasks,
|
||||||
|
now: AppTime.now(),
|
||||||
|
);
|
||||||
|
final rows = buildDayRows(
|
||||||
|
userId: programmer.id,
|
||||||
|
day: sheet.workDate,
|
||||||
|
tasks: data.tasks,
|
||||||
|
logsByTask: data.logsByTask,
|
||||||
|
workLogs: data.workLogs,
|
||||||
|
projectNames: const {},
|
||||||
|
profileNames: profileNames,
|
||||||
|
now: AppTime.now(),
|
||||||
|
);
|
||||||
|
liveRowsByDay[sheet.workDate] = rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildAccomplishmentReportData(
|
||||||
|
programmer: programmer,
|
||||||
|
range: range,
|
||||||
|
includeUnapproved: includeUnapproved,
|
||||||
|
sheets: sheets,
|
||||||
|
liveRowsByDay: liveRowsByDay,
|
||||||
|
tasks: tasks,
|
||||||
|
profileNames: profileNames,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show the Accomplishment Report generate dialog.
|
||||||
|
///
|
||||||
|
/// - [programmerId] non-null: admin mode locked to that programmer.
|
||||||
|
/// - [programmerId] null + isStrictAdmin: admin picks from all programmers.
|
||||||
|
/// - [programmerId] null + not admin: programmer self-service (own profile).
|
||||||
|
Future<void> showAccomplishmentReportDialog(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref, {
|
||||||
|
String? programmerId,
|
||||||
|
}) {
|
||||||
|
return showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => _AccomplishmentReportDialog(programmerId: programmerId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Dialog widget
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class _AccomplishmentReportDialog extends ConsumerStatefulWidget {
|
||||||
|
const _AccomplishmentReportDialog({this.programmerId});
|
||||||
|
|
||||||
|
final String? programmerId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<_AccomplishmentReportDialog> createState() =>
|
||||||
|
_AccomplishmentReportDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AccomplishmentReportDialogState
|
||||||
|
extends ConsumerState<_AccomplishmentReportDialog> {
|
||||||
|
Profile? _selectedProgrammer;
|
||||||
|
late ReportDateRange _dateRange;
|
||||||
|
bool _includeUnapproved = false;
|
||||||
|
bool _generating = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_dateRange = thisMonthRange(AppTime.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onGenerate() async {
|
||||||
|
final profiles = ref.read(profilesProvider).valueOrNull ?? [];
|
||||||
|
final isStrictAdmin = ref.read(isStrictAdminProvider);
|
||||||
|
final isAdminMode = widget.programmerId != null || isStrictAdmin;
|
||||||
|
|
||||||
|
Profile? programmer;
|
||||||
|
if (widget.programmerId != null) {
|
||||||
|
for (final p in profiles) {
|
||||||
|
if (p.id == widget.programmerId) {
|
||||||
|
programmer = p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (isAdminMode) {
|
||||||
|
programmer = _selectedProgrammer;
|
||||||
|
} else {
|
||||||
|
final userId = ref.read(currentUserIdProvider);
|
||||||
|
for (final p in profiles) {
|
||||||
|
if (p.id == userId) {
|
||||||
|
programmer = p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (programmer == null) return;
|
||||||
|
|
||||||
|
setState(() => _generating = true);
|
||||||
|
try {
|
||||||
|
final data = await loadAccomplishmentReport(
|
||||||
|
ref,
|
||||||
|
programmer: programmer,
|
||||||
|
range: _dateRange,
|
||||||
|
includeUnapproved: _includeUnapproved,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
final name = data.programmerName;
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
await showPdfPreviewDialog(
|
||||||
|
context,
|
||||||
|
title: 'Accomplishment Report – $name',
|
||||||
|
filename: 'Accomplishment Report - $name.pdf',
|
||||||
|
buildBytes: () => buildAccomplishmentReportPdf(data),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _generating = false);
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(SnackBar(content: Text(e.toString())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final profiles = ref.watch(profilesProvider).valueOrNull ?? [];
|
||||||
|
final isStrictAdmin = ref.watch(isStrictAdminProvider);
|
||||||
|
final isAdminMode = widget.programmerId != null || isStrictAdmin;
|
||||||
|
|
||||||
|
final programmerProfiles =
|
||||||
|
profiles.where((p) => p.role == 'programmer').toList()
|
||||||
|
..sort((a, b) => a.fullName.compareTo(b.fullName));
|
||||||
|
|
||||||
|
// Generate is disabled when in admin-picker mode and nothing chosen yet.
|
||||||
|
final canGenerate = !_generating &&
|
||||||
|
(isAdminMode && widget.programmerId == null
|
||||||
|
? _selectedProgrammer != null
|
||||||
|
: true);
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Accomplishment Report'),
|
||||||
|
content: SizedBox(
|
||||||
|
width: 400,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// Programmer picker — only in admin-picker mode.
|
||||||
|
if (isAdminMode && widget.programmerId == null) ...[
|
||||||
|
_ProgrammerPicker(
|
||||||
|
programmers: programmerProfiles,
|
||||||
|
value: _selectedProgrammer,
|
||||||
|
onChanged: (p) => setState(() => _selectedProgrammer = p),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
],
|
||||||
|
// Date range filter.
|
||||||
|
ReportDateFilter.controlled(
|
||||||
|
value: _dateRange,
|
||||||
|
onChanged: (r) => setState(() => _dateRange = r),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
// Include-unapproved toggle.
|
||||||
|
SwitchListTile(
|
||||||
|
title: const Text('Include unapproved days (marked)'),
|
||||||
|
value: _includeUnapproved,
|
||||||
|
onChanged: (v) => setState(() => _includeUnapproved = v),
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
dense: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _generating ? null : () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: canGenerate ? _onGenerate : null,
|
||||||
|
child: _generating
|
||||||
|
? const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Text('Generate'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Programmer picker widget
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class _ProgrammerPicker extends StatelessWidget {
|
||||||
|
const _ProgrammerPicker({
|
||||||
|
required this.programmers,
|
||||||
|
required this.value,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<Profile> programmers;
|
||||||
|
final Profile? value;
|
||||||
|
final ValueChanged<Profile?> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InputDecorator(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Programmer',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
isDense: true,
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
),
|
||||||
|
child: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton<Profile>(
|
||||||
|
value: value,
|
||||||
|
hint: const Text('Select programmer'),
|
||||||
|
isExpanded: true,
|
||||||
|
items: programmers
|
||||||
|
.map(
|
||||||
|
(p) => DropdownMenuItem<Profile>(
|
||||||
|
value: p,
|
||||||
|
child: Text(p.fullName),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onChanged: onChanged,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'package:tasq/models/profile.model.dart';
|
||||||
|
import 'package:tasq/providers/profile_provider.dart';
|
||||||
|
import 'package:tasq/screens/programmer_tasks/report/accomplishment_report_dialog.dart';
|
||||||
|
import 'package:tasq/theme/app_theme.dart';
|
||||||
|
import 'package:tasq/utils/app_time.dart';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Widget _buildApp(Widget child, {List<Override> overrides = const []}) {
|
||||||
|
return ProviderScope(
|
||||||
|
overrides: overrides,
|
||||||
|
child: MaterialApp(
|
||||||
|
theme: AppTheme.light(),
|
||||||
|
home: Scaffold(body: child),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opener widget that shows the dialog on tap.
|
||||||
|
class _DialogOpener extends ConsumerWidget {
|
||||||
|
const _DialogOpener({this.programmerId});
|
||||||
|
|
||||||
|
final String? programmerId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return ElevatedButton(
|
||||||
|
onPressed: () => showAccomplishmentReportDialog(
|
||||||
|
context,
|
||||||
|
ref,
|
||||||
|
programmerId: programmerId,
|
||||||
|
),
|
||||||
|
child: const Text('Open'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
setUpAll(AppTime.initialize);
|
||||||
|
|
||||||
|
// ── Programmer mode (not admin, no programmerId) ─────────────────────────
|
||||||
|
|
||||||
|
group('as programmer (not admin)', () {
|
||||||
|
late List<Override> overrides;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
overrides = [
|
||||||
|
isStrictAdminProvider.overrideWithValue(false),
|
||||||
|
profilesProvider.overrideWith(
|
||||||
|
(ref) => Stream.value([
|
||||||
|
Profile(id: 'prog-1', role: 'programmer', fullName: 'Alice'),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
currentUserIdProvider.overrideWithValue('prog-1'),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('shows Accomplishment Report title', (tester) async {
|
||||||
|
await tester
|
||||||
|
.pumpWidget(_buildApp(const _DialogOpener(), overrides: overrides));
|
||||||
|
await tester.tap(find.text('Open'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Accomplishment Report'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('does not show programmer picker', (tester) async {
|
||||||
|
await tester
|
||||||
|
.pumpWidget(_buildApp(const _DialogOpener(), overrides: overrides));
|
||||||
|
await tester.tap(find.text('Open'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(DropdownButton<Profile>), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('toggle Include unapproved days is present and off by default',
|
||||||
|
(tester) async {
|
||||||
|
await tester
|
||||||
|
.pumpWidget(_buildApp(const _DialogOpener(), overrides: overrides));
|
||||||
|
await tester.tap(find.text('Open'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Include unapproved days (marked)'), findsOneWidget);
|
||||||
|
final sw = tester.widget<Switch>(find.byType(Switch).first);
|
||||||
|
expect(sw.value, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('date filter defaults to This Month', (tester) async {
|
||||||
|
await tester
|
||||||
|
.pumpWidget(_buildApp(const _DialogOpener(), overrides: overrides));
|
||||||
|
await tester.tap(find.text('Open'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('This Month'), findsOneWidget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Admin picker mode (isStrictAdmin, no programmerId passed) ────────────
|
||||||
|
|
||||||
|
group('as admin (no programmerId, strict admin)', () {
|
||||||
|
final prog1 = Profile(id: 'prog-1', role: 'programmer', fullName: 'Alice');
|
||||||
|
final prog2 = Profile(id: 'prog-2', role: 'programmer', fullName: 'Bob');
|
||||||
|
final admin1 =
|
||||||
|
Profile(id: 'admin-1', role: 'admin', fullName: 'Admin User');
|
||||||
|
|
||||||
|
late List<Override> overrides;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
overrides = [
|
||||||
|
isStrictAdminProvider.overrideWithValue(true),
|
||||||
|
profilesProvider.overrideWith(
|
||||||
|
(ref) => Stream.value([prog1, prog2, admin1]),
|
||||||
|
),
|
||||||
|
currentUserIdProvider.overrideWithValue('admin-1'),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('shows programmer picker', (tester) async {
|
||||||
|
await tester
|
||||||
|
.pumpWidget(_buildApp(const _DialogOpener(), overrides: overrides));
|
||||||
|
await tester.tap(find.text('Open'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(DropdownButton<Profile>), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Generate is disabled until a programmer is chosen',
|
||||||
|
(tester) async {
|
||||||
|
await tester
|
||||||
|
.pumpWidget(_buildApp(const _DialogOpener(), overrides: overrides));
|
||||||
|
await tester.tap(find.text('Open'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final button = tester.widget<FilledButton>(
|
||||||
|
find.ancestor(
|
||||||
|
of: find.text('Generate'),
|
||||||
|
matching: find.byType(FilledButton),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(button.onPressed, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('picker shows only programmer-role profiles', (tester) async {
|
||||||
|
await tester
|
||||||
|
.pumpWidget(_buildApp(const _DialogOpener(), overrides: overrides));
|
||||||
|
await tester.tap(find.text('Open'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Open the dropdown to expose its items.
|
||||||
|
await tester.tap(find.byType(DropdownButton<Profile>));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Programmer names should appear in the dropdown overlay.
|
||||||
|
expect(find.text('Alice'), findsWidgets);
|
||||||
|
expect(find.text('Bob'), findsWidgets);
|
||||||
|
// Admin-role profile must not appear.
|
||||||
|
expect(find.text('Admin User'), findsNothing);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user