Files
tasq/test/profile_screen_test.dart
T
redz1029 3cb980b629 QA hardening: security RLS fixes, Flutter 3.47.5 upgrade, UI/validation fixes
Security — enforce write authorization server-side (was UI/RPC-only):
- it_service_requests RLS: block cross-office read/edit + self-approve (QA-015)
- pass_slips RLS: owner can complete but not self-approve (QA-046)
- swap_requests RLS: scope select/update to participants + admin (QA-047)
- storage: tighten it_service_attachments + task_attachments write/delete (QA-027)
- admin_user_management edge function: allow programmers to manage users (QA-016)

Fixes:
- workforce generator "uncovered shifts" false alarms (QA-043/044)
- network-map VLAN + New-location dialog validation, disabled-until-valid (QA-048)
- de-flake time-of-day-dependent dashboard metrics test (QA-045)

Toolchain:
- upgrade to Flutter 3.47.5 / Dart 3.13.4; font_awesome_flutter 11.0.0,
  flutter_quill 11.6.0, pdfrx 2.6.5; clear resulting deprecations (QA-002)

analyze clean; 139 tests pass; web build succeeds. Report + evidence in docs/qa/.

Note: also carries the in-progress Brick model cleanup already present in the
working tree. QA-001 (AI keys public in the build) is deferred by owner decision.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-09-26 11:52:16 +08:00

233 lines
6.8 KiB
Dart

import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tasq/models/office.model.dart';
import 'package:tasq/models/profile.model.dart';
import 'package:tasq/models/user_office.dart';
import 'package:tasq/providers/profile_provider.dart';
import 'package:tasq/providers/tickets_provider.dart';
import 'package:tasq/providers/user_offices_provider.dart';
import 'package:tasq/providers/auth_provider.dart';
import 'package:tasq/screens/profile/profile_screen.dart';
import 'package:tasq/widgets/multi_select_picker.dart';
import 'package:awesome_snackbar_content/awesome_snackbar_content.dart';
class _FakeProfileController implements ProfileController {
String? lastFullName;
String? lastPassword;
@override
Future<void> updateFullName({
required String userId,
required String fullName,
}) async {
lastFullName = fullName;
}
@override
Future<void> updatePassword(String password) async {
lastPassword = password;
}
@override
Future<String> uploadAvatar({
required String userId,
required Uint8List bytes,
required String fileName,
}) async {
return 'https://example.com/avatar.jpg';
}
@override
Future<String> uploadFacePhoto({
required String userId,
required Uint8List bytes,
required String fileName,
}) async {
return 'https://example.com/face.jpg';
}
@override
Future<Uint8List?> downloadFacePhoto(String userId, {String? enrolledAt}) async {
return null;
}
}
class _FakeUserOfficesController implements UserOfficesController {
final List<Map<String, String>> assigned = [];
@override
Future<void> assignUserOffice({
required String userId,
required String officeId,
}) async {
assigned.add({'userId': userId, 'officeId': officeId});
}
@override
Future<void> removeUserOffice({
required String userId,
required String officeId,
}) async {
assigned.removeWhere(
(e) => e['userId'] == userId && e['officeId'] == officeId,
);
}
}
void main() {
final officeA = Office(id: 'office-1', name: 'HQ');
final officeB = Office(id: 'office-2', name: 'Branch');
final profile = Profile(id: 'user-1', role: 'standard', fullName: 'Paola');
ProviderScope buildApp({required List<Override> overrides}) {
return ProviderScope(
overrides: [
currentUserIdProvider.overrideWithValue(profile.id),
sessionProvider.overrideWithValue(null),
...overrides,
],
child: const MaterialApp(home: Scaffold(body: ProfileScreen())),
);
}
testWidgets('renders fields and sections', (tester) async {
await tester.pumpWidget(
buildApp(
overrides: [
currentProfileProvider.overrideWith((ref) => Stream.value(profile)),
officesProvider.overrideWith(
(ref) => Stream.value([officeA, officeB]),
),
userOfficesProvider.overrideWith(
(ref) => Stream.value(const <UserOffice>[]),
),
],
),
);
await tester.pumpAndSettle();
expect(find.text('My Profile'), findsOneWidget);
expect(find.text('Account details'), findsOneWidget);
expect(find.widgetWithText(TextFormField, 'Full name'), findsOneWidget);
expect(
find.byWidgetPredicate(
(w) => w is MultiSelectPicker<Office> && w.label == 'Offices',
),
findsOneWidget,
);
});
testWidgets('save details calls profile controller', (tester) async {
final fake = _FakeProfileController();
await tester.pumpWidget(
buildApp(
overrides: [
currentProfileProvider.overrideWith((ref) => Stream.value(profile)),
officesProvider.overrideWith((ref) => Stream.value([officeA])),
userOfficesProvider.overrideWith(
(ref) => Stream.value(const <UserOffice>[]),
),
profileControllerProvider.overrideWithValue(fake),
],
),
);
await tester.pumpAndSettle();
await tester.enterText(
find.widgetWithText(TextFormField, 'Full name'),
'New Name',
);
await tester.ensureVisible(find.text('Save details'));
await tester.tap(find.text('Save details'));
await tester.pumpAndSettle();
expect(fake.lastFullName, equals('New Name'));
// should show a success snackbar using the awesome_snackbar_content package
// (AwesomeSnackbarContent uses its own SVG icons, not Material Icons)
expect(find.byType(AwesomeSnackbarContent), findsOneWidget);
});
testWidgets('save offices assigns selected office', (tester) async {
final fakeOffices = _FakeUserOfficesController();
await tester.pumpWidget(
buildApp(
overrides: [
currentProfileProvider.overrideWith((ref) => Stream.value(profile)),
officesProvider.overrideWith(
(ref) => Stream.value([officeA, officeB]),
),
userOfficesProvider.overrideWith(
(ref) => Stream.value(const <UserOffice>[]),
),
userOfficesControllerProvider.overrideWithValue(fakeOffices),
],
),
);
await tester.pumpAndSettle();
// Open MultiSelect picker
final selectChip = find.byWidgetPredicate(
(w) => w is ActionChip && (w.label as Text).data == 'Select',
);
expect(selectChip, findsOneWidget);
await tester.ensureVisible(selectChip);
await tester.tap(selectChip);
await tester.pumpAndSettle();
// In the dialog, tap the first office checkbox and press Done
await tester.tap(find.text('HQ'));
await tester.pumpAndSettle();
await tester.tap(find.text('Done'));
await tester.pumpAndSettle();
// Save offices
await tester.tap(find.text('Save offices'));
await tester.pumpAndSettle();
expect(fakeOffices.assigned, isNotEmpty);
expect(fakeOffices.assigned.first['officeId'], equals('office-1'));
});
testWidgets('change password calls controller', (tester) async {
final fake = _FakeProfileController();
await tester.pumpWidget(
buildApp(
overrides: [
currentProfileProvider.overrideWith((ref) => Stream.value(profile)),
officesProvider.overrideWith((ref) => Stream.value([officeA])),
userOfficesProvider.overrideWith(
(ref) => Stream.value(const <UserOffice>[]),
),
profileControllerProvider.overrideWithValue(fake),
],
),
);
await tester.pumpAndSettle();
await tester.enterText(
find.widgetWithText(TextFormField, 'New password'),
'new-pass-123',
);
await tester.enterText(
find.widgetWithText(TextFormField, 'Confirm password'),
'new-pass-123',
);
await tester.ensureVisible(find.text('Change password'));
await tester.tap(find.text('Change password'));
await tester.pumpAndSettle();
expect(fake.lastPassword, equals('new-pass-123'));
});
}