96fcb9ab5e
Rework .agents/, Claude/Codex config, CI workflows, lint gates and lint-rule tests, and the release/changelog tooling. The 5% comment density gate stays diff-scoped inside git hooks, where GIT_DIR outranks the -C the gate uses to locate the repository. The tray icon generator writes multi-size ICO files from the SVG sources into the per-platform tray asset directories. The custom code-review subagents are gone: the built-in /code-review never dispatched them, and the subagent model now lives in a user-level setting outside the repository. The Makefile, build_config.yaml and CI hand the Core and Helper builds to the setup build hook, so the buildkit launchers go, and setup.dart packages AppImage and rpm on arm64 with flutter_distributor pinned to the fork tag that normalizes packaging permissions. Changelog: skip
79 lines
2.5 KiB
Dart
79 lines
2.5 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:test/test.dart';
|
|
|
|
/// The one button that carries its label on an enclosing [Tooltip] instead of
|
|
/// its own `tooltip:`. Nesting a second tooltip inside would fight it.
|
|
const _wrappedInTooltip = 'lib/views/dashboard/widgets/core_status_button.dart';
|
|
|
|
final _iconButton = RegExp(
|
|
r'\bIconButton(?:\.(?:filled|filledTonal|outlined))?\(',
|
|
);
|
|
|
|
Iterable<File> _dartFilesIn(String root) sync* {
|
|
final directory = Directory(root);
|
|
if (!directory.existsSync()) {
|
|
fail('$root no longer exists; update this test.');
|
|
}
|
|
for (final entity in directory.listSync(recursive: true)) {
|
|
if (entity is File &&
|
|
entity.path.endsWith('.dart') &&
|
|
!entity.path.endsWith('.g.dart') &&
|
|
!entity.path.endsWith('.freezed.dart') &&
|
|
!entity.path.contains('/generated/')) {
|
|
yield entity;
|
|
}
|
|
}
|
|
}
|
|
|
|
String _arguments(String source, int start) {
|
|
var depth = 1;
|
|
var index = start;
|
|
while (index < source.length && depth > 0) {
|
|
if (source[index] == '(') depth++;
|
|
if (source[index] == ')') depth--;
|
|
index++;
|
|
}
|
|
return source.substring(start, index - 1);
|
|
}
|
|
|
|
void main() {
|
|
test('every icon-only IconButton carries a tooltip', () {
|
|
final unlabelled = <String>[];
|
|
|
|
for (final file in _dartFilesIn('lib')) {
|
|
final source = file.readAsStringSync();
|
|
if (file.path == _wrappedInTooltip) continue;
|
|
|
|
for (final match in _iconButton.allMatches(source)) {
|
|
final arguments = _arguments(source, match.end);
|
|
if (RegExp(r'\btooltip\s*:').hasMatch(arguments)) continue;
|
|
// An icon slot holding text is already its own visible label.
|
|
if (RegExp(r'icon:\s*Text\(').hasMatch(arguments)) continue;
|
|
|
|
final line = '\n'.allMatches(source.substring(0, match.start)).length;
|
|
unlabelled.add(
|
|
'${file.path}:${line + 1} — an icon has no accessible name, so '
|
|
'TalkBack and VoiceOver announce nothing and the desktop build shows '
|
|
'no hover hint.',
|
|
);
|
|
}
|
|
}
|
|
|
|
expect(unlabelled, isEmpty, reason: unlabelled.join('\n'));
|
|
});
|
|
|
|
test('the exempted button still gets its label from an enclosing Tooltip', () {
|
|
final source = File(_wrappedInTooltip).readAsStringSync();
|
|
|
|
expect(
|
|
source,
|
|
contains('Tooltip('),
|
|
reason:
|
|
'$_wrappedInTooltip is exempted from the tooltip rule because an '
|
|
'enclosing Tooltip labels it. That wrapper is gone; either restore it '
|
|
'or give the button its own tooltip and drop the exemption.',
|
|
);
|
|
});
|
|
}
|