mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:07:04 +01:00
Split the last over-cap focus_owner Dart test file
status_page_test.dart 309 -> 175, with the release and polling tests in status_page_actions_test.dart. An intermediate run showed 97 rather than 94 - the tell that a git checkout had restored the original while the split copy still held the same three tests. Counting testWidgets() calls in both halves (5 + 3 = 8) confirmed the fix. status_page_state.dart stays at 307: it is one State class whose methods read seven private fields, so a mixin cannot see them without declaring the state twice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
This commit is contained in:
parent
d9982e53dd
commit
3c2c2baba0
@ -0,0 +1,37 @@
|
||||
{
|
||||
"intent": "Bring the last over-cap focus_owner Dart test file under the 250-line cap, and record why status_page_state.dart cannot follow.",
|
||||
"scope": [
|
||||
"test/status_page_test.dart (309 -> 175) + status_page_actions_test.dart",
|
||||
"Not split: lib/status_page_state.dart (307)",
|
||||
"Non-goal: no assertion changed"
|
||||
],
|
||||
"changes": [
|
||||
"The release, enforcement and polling widget tests moved to status_page_actions_test.dart, taking the _logLine helper with them since nothing in the original half still used it.",
|
||||
"status_page_state.dart was attempted and reverted. It is one State class whose methods call setState and read seven private fields; extracting them as a mixin gives 'Undefined name _busy' because a mixin cannot see the concrete class's fields, and making it work would mean declaring those fields twice."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "flutter test",
|
||||
"result": "pass",
|
||||
"evidence": "94 passed, matching the baseline. An intermediate run showed 97, which was the tell that a git checkout had restored the original file while the split copy still held the same three tests - caught by comparing the count rather than trusting a green run."
|
||||
},
|
||||
{
|
||||
"command": "grep -c 'testWidgets(' on both halves",
|
||||
"result": "pass",
|
||||
"evidence": "5 and 3, summing to the 8 the original had. Counting the cases directly is what confirmed the duplicates were gone."
|
||||
},
|
||||
{
|
||||
"command": "flutter analyze",
|
||||
"result": "pass",
|
||||
"evidence": "'No issues found!' after removing the now-unused _logLine and a dart:convert import."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"status_page_state.dart stays at 307 lines. Splitting it needs the State's fields moved into the mixin as well, which changes where the widget's state actually lives - a design change, not a line-count split.",
|
||||
"An automated unused-declaration pass removed _logLine while it was still in use and broke two tests; it was reverted and redone by hand. Analyzer 'unused' output is per-file and cannot see a sibling test file."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit; status_page_test.dart returns to 309 lines and the new file disappears.",
|
||||
"After rollback, re-run flutter test and expect 94 passed."
|
||||
]
|
||||
}
|
||||
204
focus_owner/test/status_page_actions_test.dart
Normal file
204
focus_owner/test/status_page_actions_test.dart
Normal file
@ -0,0 +1,204 @@
|
||||
// StatusPage release, enforcement and polling behaviour.
|
||||
|
||||
// StatusPage widget behaviour.
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:focus_owner/device_policy.dart';
|
||||
import 'package:focus_owner/main.dart';
|
||||
import 'package:focus_owner/policy.dart';
|
||||
|
||||
/// Builds a channel whose handler answers from [responses], recording calls.
|
||||
({MethodChannel channel, List<MethodCall> calls}) _fakeChannel(
|
||||
Map<String, Object?> responses,
|
||||
) {
|
||||
const channel = MethodChannel('test/device_policy');
|
||||
final calls = <MethodCall>[];
|
||||
// hasHomeLocation is read on every status refresh, so it is defaulted here
|
||||
// rather than restated in each widget test. An explicit stub still wins,
|
||||
// which is what the geofence-state tests rely on.
|
||||
final stubs = {'hasHomeLocation': false, ...responses};
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (call) async {
|
||||
calls.add(call);
|
||||
if (!stubs.containsKey(call.method)) {
|
||||
throw PlatformException(code: 'unimplemented', message: 'no stub');
|
||||
}
|
||||
return stubs[call.method];
|
||||
});
|
||||
return (channel: channel, calls: calls);
|
||||
}
|
||||
|
||||
Map<String, Object?> _statusMap({bool isDeviceOwner = false}) => {
|
||||
'packageName': 'com.kuhy.focus_owner',
|
||||
'isDeviceOwner': isDeviceOwner,
|
||||
'isAdminActive': isDeviceOwner,
|
||||
'sdkInt': 36,
|
||||
'restrictionsApplied': false,
|
||||
};
|
||||
|
||||
/// Minimal policy so widget tests never touch the (empty) test rootBundle.
|
||||
Future<FocusPolicy> _stubPolicy() async => FocusPolicy.fromJson({
|
||||
'schema_version': kSupportedSchemaVersion,
|
||||
'home': {
|
||||
'latitude': null,
|
||||
'longitude': null,
|
||||
'radius_m': 150.0,
|
||||
'hysteresis_m': 30.0,
|
||||
},
|
||||
'curfew': {'start': '23:00', 'end': '05:00'},
|
||||
'launcher_package': 'com.launcher',
|
||||
'allowed_packages': ['com.good'],
|
||||
'night_allowed_packages': <String>[],
|
||||
'never_disable_prefixes': <String>[],
|
||||
'workout_unblock_domains': ['youtube.com'],
|
||||
'browser_packages': <String>[],
|
||||
});
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('StatusPage actions', () {
|
||||
testWidgets('confirming release calls the platform', (tester) async {
|
||||
final fake = _fakeChannel({
|
||||
'status': _statusMap(isDeviceOwner: true),
|
||||
'releaseDeviceOwner': true,
|
||||
});
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: StatusPage(
|
||||
policy: DevicePolicy(fake.channel),
|
||||
loadFocusPolicy: _stubPolicy,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.scrollUntilVisible(find.text('Release device owner'), 100);
|
||||
await tester.tap(find.text('Release device owner'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Release'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
fake.calls.where((c) => c.method == 'releaseDeviceOwner'),
|
||||
hasLength(1),
|
||||
);
|
||||
expect(find.text('Device owner released'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('running enforcement waits for the new record', (tester) async {
|
||||
// The pass runs in a separate service process and now waits for a
|
||||
// location fix, so re-reading immediately returns the PREVIOUS record.
|
||||
// Measured: after re-anchoring home the card still showed the old
|
||||
// distance, which reads as "the button did nothing".
|
||||
var passRan = false;
|
||||
const channel = MethodChannel('test/refresh');
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (call) async {
|
||||
switch (call.method) {
|
||||
case 'status':
|
||||
return _statusMap(isDeviceOwner: true);
|
||||
case 'hasHomeLocation':
|
||||
return true;
|
||||
case 'runEnforcementNow':
|
||||
passRan = true;
|
||||
return true;
|
||||
case 'readEnforcementLog':
|
||||
return [_logLine(passRan ? 'AT_HOME' : 'AWAY')];
|
||||
default:
|
||||
throw PlatformException(code: 'unimplemented');
|
||||
}
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: StatusPage(
|
||||
policy: DevicePolicy(channel),
|
||||
loadFocusPolicy: _stubPolicy,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('AWAY'), findsOneWidget);
|
||||
|
||||
await tester.scrollUntilVisible(find.text('Run enforcement now'), 100);
|
||||
await tester.tap(find.text('Run enforcement now'));
|
||||
// Long enough for at least one poll interval to elapse.
|
||||
await tester.pump(const Duration(seconds: 3));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('AT HOME'), findsOneWidget);
|
||||
expect(find.text('AWAY'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('a stuck pass stops polling instead of hanging',
|
||||
(tester) async {
|
||||
// If no new record ever lands the UI must still settle, not spin.
|
||||
const channel = MethodChannel('test/stuck');
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (call) async {
|
||||
switch (call.method) {
|
||||
case 'status':
|
||||
return _statusMap(isDeviceOwner: true);
|
||||
case 'hasHomeLocation':
|
||||
return true;
|
||||
case 'runEnforcementNow':
|
||||
return true;
|
||||
case 'readEnforcementLog':
|
||||
return [_logLine('AWAY')];
|
||||
default:
|
||||
throw PlatformException(code: 'unimplemented');
|
||||
}
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: StatusPage(
|
||||
policy: DevicePolicy(channel),
|
||||
loadFocusPolicy: _stubPolicy,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.scrollUntilVisible(find.text('Run enforcement now'), 100);
|
||||
await tester.tap(find.text('Run enforcement now'));
|
||||
// Past the polling budget (~30 s).
|
||||
await tester.pump(const Duration(seconds: 40));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('AWAY'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// One JSON-lines record, as the platform channel returns them.
|
||||
String _logLine(String reason) => jsonEncode({
|
||||
'ts': DateTime.now().millisecondsSinceEpoch,
|
||||
'reason': reason,
|
||||
'distance_m': reason == 'AWAY' ? 766.0 : 0.0,
|
||||
'threshold_m': 150.0,
|
||||
'inside_fence': reason != 'AWAY',
|
||||
'home_configured': true,
|
||||
'fix': {
|
||||
'age_ms': 5000,
|
||||
'provider': 'gps',
|
||||
'accuracy_m': 11.0,
|
||||
'outcome': 'ACTIVE_OK',
|
||||
},
|
||||
'curfew_active': false,
|
||||
'curfew_window': '23:00-05:00',
|
||||
'counts': {
|
||||
'to_hide': 3,
|
||||
'to_show': 58,
|
||||
'hid_delta': 0,
|
||||
'restored_delta': 0,
|
||||
},
|
||||
'hidden': const <Object?>[],
|
||||
'failure': null,
|
||||
});
|
||||
@ -1,6 +1,5 @@
|
||||
// StatusPage widget behaviour.
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -167,143 +166,9 @@ void main() {
|
||||
isEmpty,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('confirming release calls the platform', (tester) async {
|
||||
final fake = _fakeChannel({
|
||||
'status': _statusMap(isDeviceOwner: true),
|
||||
'releaseDeviceOwner': true,
|
||||
});
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: StatusPage(
|
||||
policy: DevicePolicy(fake.channel),
|
||||
loadFocusPolicy: _stubPolicy,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.scrollUntilVisible(find.text('Release device owner'), 100);
|
||||
await tester.tap(find.text('Release device owner'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Release'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
fake.calls.where((c) => c.method == 'releaseDeviceOwner'),
|
||||
hasLength(1),
|
||||
);
|
||||
expect(find.text('Device owner released'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('running enforcement waits for the new record', (tester) async {
|
||||
// The pass runs in a separate service process and now waits for a
|
||||
// location fix, so re-reading immediately returns the PREVIOUS record.
|
||||
// Measured: after re-anchoring home the card still showed the old
|
||||
// distance, which reads as "the button did nothing".
|
||||
var passRan = false;
|
||||
const channel = MethodChannel('test/refresh');
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (call) async {
|
||||
switch (call.method) {
|
||||
case 'status':
|
||||
return _statusMap(isDeviceOwner: true);
|
||||
case 'hasHomeLocation':
|
||||
return true;
|
||||
case 'runEnforcementNow':
|
||||
passRan = true;
|
||||
return true;
|
||||
case 'readEnforcementLog':
|
||||
return [_logLine(passRan ? 'AT_HOME' : 'AWAY')];
|
||||
default:
|
||||
throw PlatformException(code: 'unimplemented');
|
||||
}
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: StatusPage(
|
||||
policy: DevicePolicy(channel),
|
||||
loadFocusPolicy: _stubPolicy,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('AWAY'), findsOneWidget);
|
||||
|
||||
await tester.scrollUntilVisible(find.text('Run enforcement now'), 100);
|
||||
await tester.tap(find.text('Run enforcement now'));
|
||||
// Long enough for at least one poll interval to elapse.
|
||||
await tester.pump(const Duration(seconds: 3));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('AT HOME'), findsOneWidget);
|
||||
expect(find.text('AWAY'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('a stuck pass stops polling instead of hanging',
|
||||
(tester) async {
|
||||
// If no new record ever lands the UI must still settle, not spin.
|
||||
const channel = MethodChannel('test/stuck');
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (call) async {
|
||||
switch (call.method) {
|
||||
case 'status':
|
||||
return _statusMap(isDeviceOwner: true);
|
||||
case 'hasHomeLocation':
|
||||
return true;
|
||||
case 'runEnforcementNow':
|
||||
return true;
|
||||
case 'readEnforcementLog':
|
||||
return [_logLine('AWAY')];
|
||||
default:
|
||||
throw PlatformException(code: 'unimplemented');
|
||||
}
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: StatusPage(
|
||||
policy: DevicePolicy(channel),
|
||||
loadFocusPolicy: _stubPolicy,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.scrollUntilVisible(find.text('Run enforcement now'), 100);
|
||||
await tester.tap(find.text('Run enforcement now'));
|
||||
// Past the polling budget (~30 s).
|
||||
await tester.pump(const Duration(seconds: 40));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('AWAY'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// One JSON-lines record, as the platform channel returns them.
|
||||
String _logLine(String reason) => jsonEncode({
|
||||
'ts': DateTime.now().millisecondsSinceEpoch,
|
||||
'reason': reason,
|
||||
'distance_m': reason == 'AWAY' ? 766.0 : 0.0,
|
||||
'threshold_m': 150.0,
|
||||
'inside_fence': reason != 'AWAY',
|
||||
'home_configured': true,
|
||||
'fix': {
|
||||
'age_ms': 5000,
|
||||
'provider': 'gps',
|
||||
'accuracy_m': 11.0,
|
||||
'outcome': 'ACTIVE_OK',
|
||||
},
|
||||
'curfew_active': false,
|
||||
'curfew_window': '23:00-05:00',
|
||||
'counts': {
|
||||
'to_hide': 3,
|
||||
'to_show': 58,
|
||||
'hid_delta': 0,
|
||||
'restored_delta': 0,
|
||||
},
|
||||
'hidden': const <Object?>[],
|
||||
'failure': null,
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user