Files
FlClash/test/features/rule_dialog_test.dart
T
chen08209 0bac50029a feat(profiles): manage external resources from the profile details
Changelog: Profile-detail resources and include-all in custom groups
2026-10-17 17:21:43 +08:00

305 lines
8.8 KiB
Dart

import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/features/form/form.dart';
import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/ui/ui.dart';
import 'package:flutter/rendering.dart';
import 'package:material_ui/material_ui.dart';
import 'package:flutter_test/flutter_test.dart';
import '../helpers/test_app.dart';
Finder _banner(String text) =>
find.descendant(of: find.byType(ErrorBanner), matching: find.text(text));
void main() {
testWidgets('add rule validates content and returns the entered rule', (
tester,
) async {
Rule? result;
await tester.pumpWidget(
TestApp(
wrapInProviderScope: true,
overrides: [
viewSizeProvider.overrideWithBuild((_, _) => const Size(1200, 800)),
],
child: Builder(
builder: (context) {
return FilledButton(
onPressed: () async {
result = await showFormSheet<Rule>(
context: context,
builder: (_) => const RuleForm(),
);
},
child: const Text('open'),
);
},
),
),
);
await tester.tap(find.text('open'));
await tester.pumpAndSettle();
final l10n = AppLocalizations.current;
await tester.tap(find.widgetWithText(TextButton, l10n.save));
await tester.pumpAndSettle();
expect(_banner(l10n.emptyTip(l10n.content)), findsOneWidget);
expect(result, isNull);
await tester.enterText(find.byType(TextFormField), 'example.com');
await tester.pumpAndSettle();
expect(find.byType(ErrorBanner), findsNothing);
await tester.tap(find.widgetWithText(TextButton, l10n.save));
await tester.pumpAndSettle();
expect(find.byType(RuleForm), findsNothing);
expect(result, isNotNull);
expect(result!.ruleAction, RuleAction.DOMAIN);
expect(result!.content, 'example.com');
expect(result!.ruleTarget, isNotEmpty);
await tester.pumpWidget(const SizedBox.shrink());
});
testWidgets('every built-in target is a valid rule target', (tester) async {
for (final target in RuleTarget.baseTargetNames) {
await tester.pumpWidget(
TestApp(
child: Material(
child: RuleItem(
isSelected: false,
rule: Rule(content: 'example.com', ruleTarget: target),
onSelected: () {},
onEdit: (_) {},
),
),
),
);
expect(find.text(target), findsOneWidget);
expect(find.byType(IconButton), findsNothing, reason: target);
}
await tester.pumpWidget(const SizedBox.shrink());
});
testWidgets('a rule without content centers its action', (tester) async {
await tester.pumpWidget(
TestApp(
child: Material(
child: Center(
child: SizedBox(
width: 600,
height: 64,
child: RuleItem(
isSelected: false,
rule: const Rule(
ruleAction: RuleAction.MATCH,
ruleTarget: 'DIRECT',
),
onSelected: () {},
onEdit: (_) {},
),
),
),
),
),
);
expect(
tester.getCenter(find.text(RuleAction.MATCH.name)).dy,
moreOrLessEquals(tester.getCenter(find.byType(RuleItem)).dy, epsilon: 1),
);
await tester.pumpWidget(const SizedBox.shrink());
});
testWidgets('a long target yields width to the action on one line', (
tester,
) async {
await tester.pumpWidget(
TestApp(
child: Material(
child: Center(
child: SizedBox(
width: 480,
height: 64,
child: RuleItem(
isSelected: false,
rule: const Rule(
ruleAction: RuleAction.DOMAIN_KEYWORD,
content: 'example',
ruleTarget: 'Singapore-Auto-Select',
),
onSelected: () {},
onEdit: (_) {},
),
),
),
),
),
);
final action = tester.renderObject<RenderParagraph>(
find.text(RuleAction.DOMAIN_KEYWORD.name),
);
expect(action.didExceedMaxLines, isFalse);
expect(action.size.height, action.getMinIntrinsicHeight(double.infinity));
expect(tester.takeException(), isNull);
await tester.pumpWidget(const SizedBox.shrink());
});
testWidgets('selecting MATCH target shows the label but stores MATCH', (
tester,
) async {
Rule? result;
await tester.pumpWidget(
TestApp(
wrapInProviderScope: true,
overrides: [
viewSizeProvider.overrideWithBuild((_, _) => const Size(1200, 800)),
],
child: Builder(
builder: (context) {
return FilledButton(
onPressed: () async {
result = await showFormSheet<Rule>(
context: context,
builder: (_) => const RuleForm(),
);
},
child: const Text('open'),
);
},
),
),
);
await tester.tap(find.text('open'));
await tester.pumpAndSettle();
final l10n = AppLocalizations.current;
await tester.enterText(find.byType(TextFormField), 'example.com');
await tester.tap(find.text(l10n.splitStrategy));
await tester.pumpAndSettle();
await tester.ensureVisible(find.text(l10n.matchTarget).last);
await tester.pumpAndSettle();
await tester.tap(find.text(l10n.matchTarget).last);
await tester.pumpAndSettle();
expect(find.text(l10n.matchTarget), findsOneWidget);
await tester.tap(find.widgetWithText(TextButton, l10n.save));
await tester.pumpAndSettle();
expect(result, isNotNull);
expect(result!.ruleTarget, 'MATCH');
await tester.pumpWidget(const SizedBox.shrink());
});
testWidgets('edit rule keeps the id and order and prefills the fields', (
tester,
) async {
Rule? result;
const rule = Rule(
id: 42,
ruleAction: RuleAction.GEOIP,
content: 'CN',
ruleTarget: 'GEOIP',
noResolve: true,
order: 'a5',
);
await tester.pumpWidget(
TestApp(
wrapInProviderScope: true,
overrides: [
viewSizeProvider.overrideWithBuild((_, _) => const Size(1200, 800)),
],
child: Builder(
builder: (context) {
return FilledButton(
onPressed: () async {
result = await showFormSheet<Rule>(
context: context,
builder: (_) => const RuleForm(rule: rule),
);
},
child: const Text('open'),
);
},
),
),
);
await tester.tap(find.text('open'));
await tester.pumpAndSettle();
expect(find.text('CN'), findsOneWidget);
await tester.enterText(find.byType(TextFormField), 'US');
await tester.tap(
find.widgetWithText(TextButton, AppLocalizations.current.save),
);
await tester.pumpAndSettle();
expect(result, isNotNull);
expect(result!.id, 42);
expect(result!.order, 'a5');
expect(result!.content, 'US');
expect(result!.ruleAction, RuleAction.GEOIP);
expect(result!.noResolve, isTrue);
await tester.pumpWidget(const SizedBox.shrink());
});
testWidgets('a payload the core would refuse is held at the field', (
tester,
) async {
Rule? result;
const rule = Rule(
id: 7,
ruleAction: RuleAction.DST_PORT,
content: '443',
ruleTarget: 'DIRECT',
);
await tester.pumpWidget(
TestApp(
wrapInProviderScope: true,
overrides: [
viewSizeProvider.overrideWithBuild((_, _) => const Size(1200, 800)),
],
child: Builder(
builder: (context) {
return FilledButton(
onPressed: () async {
result = await showFormSheet<Rule>(
context: context,
builder: (_) => const RuleForm(rule: rule),
);
},
child: const Text('open'),
);
},
),
),
);
await tester.tap(find.text('open'));
await tester.pumpAndSettle();
final l10n = AppLocalizations.current;
await tester.enterText(find.byType(TextFormField), 'https');
await tester.tap(find.widgetWithText(TextButton, l10n.save));
await tester.pumpAndSettle();
expect(_banner(l10n.invalidRangeContent), findsOneWidget);
expect(result, isNull);
await tester.enterText(find.byType(TextFormField), '80/443');
await tester.tap(find.widgetWithText(TextButton, l10n.save));
await tester.pumpAndSettle();
expect(result?.content, '80/443');
await tester.pumpWidget(const SizedBox.shrink());
});
}