Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import 'package:path/path.dart' as path;
import 'package:test/test.dart';

// Benchmark size in kB.
const bundleSizeBenchmark = 5550;
const bundleSizeBenchmark = 5560;
const gzipBundleSizeBenchmark = 1650;

void main() {
Expand Down
7 changes: 7 additions & 0 deletions packages/devtools_app/lib/src/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import 'framework/notifications_view.dart';
import 'framework/observer/disconnect_observer.dart';
import 'framework/release_notes.dart';
import 'framework/scaffold/scaffold.dart';
import 'screens/accessibility/accessibility_controller.dart';
import 'screens/accessibility/accessibility_screen.dart';
import 'screens/app_size/app_size_controller.dart';
import 'screens/app_size/app_size_screen.dart';
import 'screens/debugger/debugger_controller.dart';
Expand Down Expand Up @@ -731,6 +733,11 @@ List<DevToolsScreen> defaultScreens({
DTDToolsScreen(),
createController: (_) => DTDToolsController(),
),
if (FeatureFlags.accessibility.isEnabled)
DevToolsScreen<AccessibilityController>(
AccessibilityScreen(),
createController: (_) => AccessibilityController(),
),
];
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2025 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.

import 'package:devtools_app_shared/utils.dart';
import 'package:flutter/foundation.dart';

import '../../shared/framework/screen.dart';
import '../../shared/framework/screen_controllers.dart';

class AccessibilityController extends DevToolsScreenController
with AutoDisposeControllerMixin {
@override
String get screenId => ScreenMetaData.accessibility.id;

ValueListenable<bool> get accessibilityEnabled => _accessibilityEnabled;
final _accessibilityEnabled = ValueNotifier<bool>(false);

ValueListenable<double> get textScaleFactor => _textScaleFactor;
final _textScaleFactor = ValueNotifier<double>(1.0);

ValueListenable<bool> get highContrastEnabled => _highContrastEnabled;
final _highContrastEnabled = ValueNotifier<bool>(false);

ValueListenable<bool> get autoAuditEnabled => _autoAuditEnabled;
final _autoAuditEnabled = ValueNotifier<bool>(false);

void setTextScaleFactor(double factor) {
_textScaleFactor.value = factor;
// TODO(chunhtai): set text scale factor on device.
}

void toggleHighContrast(bool enable) {
_highContrastEnabled.value = enable;
// TODO(chunhtai): set high contrast on device.
}

void toggleAutoAudit(bool enable) {
_autoAuditEnabled.value = enable;
// TODO(chunhtai): auto run audit when enabled.
}

void runAudit() {
// TODO(chunhtai): run accessibility audit.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright 2025 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.

import 'package:devtools_app_shared/ui.dart';
import 'package:flutter/material.dart';

import '../../shared/globals.dart';
import 'accessibility_controller.dart';

class AccessibilityControls extends StatelessWidget {
const AccessibilityControls({super.key});

@override
Widget build(BuildContext context) {
return Column(
children: [
const AreaPaneHeader(title: Text('Settings & Controls')),
Expanded(
child: ListView(
padding: const EdgeInsets.all(defaultSpacing),
children: const [
_SystemSimulationControls(),
SizedBox(height: defaultSpacing),
_AuditControls(),
],
),
),
],
);
}
}

class _SystemSimulationControls extends StatelessWidget {
const _SystemSimulationControls();

@override
Widget build(BuildContext context) {
final controller = screenControllers.lookup<AccessibilityController>();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'SYSTEM SIMULATION',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: denseSpacing),
ValueListenableBuilder<bool>(
valueListenable: controller.highContrastEnabled,
builder: (context, enabled, _) {
return SwitchListTile(
title: const Text('High Contrast Mode'),
value: enabled,
onChanged: (value) => controller.toggleHighContrast(value),
);
},
),
const SizedBox(height: denseSpacing),
ValueListenableBuilder<double>(
valueListenable: controller.textScaleFactor,
builder: (context, factor, _) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Text Scale Factor: ${factor.toStringAsFixed(1)}x'),
Slider(
value: factor,
min: 0.5,
max: 3.0,
divisions: 25,
label: factor.toStringAsFixed(1),
onChanged: (value) => controller.setTextScaleFactor(value),
),
],
);
},
),
],
);
}
}

class _AuditControls extends StatelessWidget {
const _AuditControls();

@override
Widget build(BuildContext context) {
final controller = screenControllers.lookup<AccessibilityController>();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('AUDIT CONTROLS', style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: denseSpacing),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
icon: const Icon(Icons.play_arrow),
label: const Text('Run Audit'),
onPressed: () => controller.runAudit(),
),
),
const SizedBox(height: denseSpacing),
ValueListenableBuilder<bool>(
valueListenable: controller.autoAuditEnabled,
builder: (context, enabled, _) {
return SwitchListTile(
title: const Text('Auto-run Audit'),
value: enabled,
onChanged: (value) => controller.toggleAutoAudit(value),
);
},
),
],
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2025 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.

import 'package:devtools_app_shared/ui.dart';
import 'package:flutter/material.dart';

class AccessibilityResults extends StatelessWidget {
const AccessibilityResults({super.key});

@override
Widget build(BuildContext context) {
return Column(
children: [
const AreaPaneHeader(title: Text('Audit Results')),
Expanded(
child: ListView.builder(
itemCount: 0,
itemBuilder: (context, index) {
return const ListTile(title: Text('Violation Placeholder'));
},
),
),
],
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2025 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.

import 'package:devtools_app_shared/ui.dart';
import 'package:flutter/material.dart';

import '../../shared/framework/screen.dart';
import '../../shared/globals.dart';
import 'accessibility_controller.dart';
import 'accessibility_controls.dart';
import 'accessibility_results.dart';

class AccessibilityScreen extends Screen {
AccessibilityScreen() : super.fromMetaData(ScreenMetaData.accessibility);

static final id = ScreenMetaData.accessibility.id;

@override
Widget buildScreenBody(BuildContext context) {
return const AccessibilityScreenBody();
}
}

class AccessibilityScreenBody extends StatefulWidget {
const AccessibilityScreenBody({super.key});

@override
State<AccessibilityScreenBody> createState() =>
_AccessibilityScreenBodyState();
}

class _AccessibilityScreenBodyState extends State<AccessibilityScreenBody> {
late final AccessibilityController controller;

@override
void initState() {
super.initState();
controller = screenControllers.lookup<AccessibilityController>();
}

@override
Widget build(BuildContext context) {
return SplitPane(
axis: Axis.horizontal,
initialFractions: const [0.3, 0.7],
children: const [AccessibilityControls(), AccessibilityResults()],
);
}
}
9 changes: 9 additions & 0 deletions packages/devtools_app/lib/src/shared/feature_flags.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ extension FeatureFlags on Never {
enabled: enableExperiments,
);

/// Flag to enable the Accessibility screen.
///
/// https://github.com/flutter/devtools/issues/9687
static final accessibility = BooleanFeatureFlag(
name: 'accessibility',
enabled: enableExperiments,
);

/// A set of all the boolean feature flags for debugging purposes.
///
/// When adding a new boolean flag, you are responsible for adding it to this
Expand All @@ -104,6 +112,7 @@ extension FeatureFlags on Never {
dapDebugging,
inspectorV2,
aiAssistant,
accessibility,
};

/// A set of all the Flutter channel feature flags for debugging purposes.
Expand Down
5 changes: 5 additions & 0 deletions packages/devtools_app/lib/src/shared/framework/screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ enum ScreenMetaData {
requiresAdvancedDeveloperMode: true,
requiresConnection: false,
),
accessibility(
'accessibility',
title: 'Accessibility',
icon: Icons.accessibility_new_rounded,
),
simple('simple');

const ScreenMetaData(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright 2024 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.

import 'package:devtools_app/src/app.dart';
import 'package:devtools_app/src/screens/accessibility/accessibility_controller.dart';
import 'package:devtools_app/src/screens/accessibility/accessibility_controls.dart';
import 'package:devtools_app/src/screens/accessibility/accessibility_results.dart';
import 'package:devtools_app/src/screens/accessibility/accessibility_screen.dart';
import 'package:devtools_app/src/service/service_manager.dart';
import 'package:devtools_app/src/shared/feature_flags.dart';
import 'package:devtools_app/src/shared/managers/banner_messages.dart';
import 'package:devtools_app/src/shared/managers/notifications.dart';
import 'package:devtools_app/src/shared/offline/offline_data.dart';
import 'package:devtools_app/src/shared/preferences/preferences.dart';
import 'package:devtools_app_shared/ui.dart';
import 'package:devtools_app_shared/utils.dart';
import 'package:devtools_test/devtools_test.dart';
import 'package:devtools_test/helpers.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
group('AccessibilityScreen', () {
late FakeServiceConnectionManager fakeServiceConnection;
late AccessibilityController controller;

setUp(() {
fakeServiceConnection = FakeServiceConnectionManager();
setGlobal(ServiceConnectionManager, fakeServiceConnection);
setGlobal(IdeTheme, IdeTheme());
setGlobal(OfflineDataController, OfflineDataController());
setGlobal(NotificationService, NotificationService());
setGlobal(BannerMessagesController, BannerMessagesController());
setGlobal(PreferencesController, PreferencesController());
FeatureFlags.accessibility.setEnabledForTests(true);
controller = AccessibilityController();
});

tearDown(() {
FeatureFlags.accessibility.setEnabledForTests(false);
});

testWidgets('builds its body', (WidgetTester tester) async {
await tester.pumpWidget(
wrapWithControllers(
Builder(builder: (context) => AccessibilityScreen().build(context)),
accessibility: controller,
),
);
expect(find.byType(AccessibilityScreenBody), findsOneWidget);
expect(find.byType(AccessibilityControls), findsOneWidget);
expect(find.byType(AccessibilityResults), findsOneWidget);
});

test('is included in defaultScreens when enabled', () {
devtoolsScreens = null;
expect(
defaultScreens().any((s) => s.screen is AccessibilityScreen),
isTrue,
);
});

test('is invalid in defaultScreens when disabled', () {
FeatureFlags.accessibility.setEnabledForTests(false);
devtoolsScreens = null;
expect(
defaultScreens().any((s) => s.screen is AccessibilityScreen),
isFalse,
);
});
});
}
Loading
Loading