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
40 changes: 29 additions & 11 deletions packages/devtools_app/lib/src/screens/debugger/controls.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,37 @@ class _DebuggingControlsState extends State<DebuggingControls>
height: defaultButtonHeight,
child: Row(
children: [
_pauseAndResumeButtons(
isPaused: serviceConnection.serviceManager.isMainIsolatePaused,
resuming: resuming,
// The debugging controls have no way to shrink further once their
// labels have already been dropped (see
// [DebuggingControls.minWidth]), so below roughly 630px the icon-only
// content still does not fit and the [Row] overflows. Making the
// controls scroll horizontally keeps every control reachable at any
// width instead of clipping them behind an overflow error. The
// libraries button stays pinned on the right, outside the scroll
// view, so it does not scroll out of reach.
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
_pauseAndResumeButtons(
isPaused:
serviceConnection.serviceManager.isMainIsolatePaused,
resuming: resuming,
),
const SizedBox(width: denseSpacing),
_stepButtons(canStep: canStep),
const SizedBox(width: denseSpacing),
BreakOnExceptionsControl(controller: controller),
if (isVmApp) ...[
const SizedBox(width: denseSpacing),
CodeStatisticsControls(controller: controller),
],
],
),
),
),
const SizedBox(width: denseSpacing),
_stepButtons(canStep: canStep),
const SizedBox(width: denseSpacing),
BreakOnExceptionsControl(controller: controller),
if (isVmApp) ...[
const SizedBox(width: denseSpacing),
CodeStatisticsControls(controller: controller),
],
const Expanded(child: SizedBox(width: denseSpacing)),
_librariesButton(),
],
),
Expand Down
4 changes: 4 additions & 0 deletions packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ TODO: Remove this section if there are not any updates.
* Fix a bug in the TextMate grammar parser that could result in code after
comments being classified as comments.
[#9921](https://github.com/flutter/devtools/pull/9921).
* Fixed an overflow in the debugging controls when the Debugger screen is
narrow, such as when DevTools is embedded in an IDE side panel. The controls
now scroll horizontally instead of overflowing.
[#9949](https://github.com/flutter/devtools/pull/9949)

## Network profiler updates

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright 2026 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/devtools_app.dart';
import 'package:devtools_app/src/screens/debugger/controls.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';
import 'package:mockito/mockito.dart';

void main() {
/// Widths the debugging controls are expected to lay out at without
/// overflowing.
///
/// The controls stop showing button labels below
/// [DebuggingControls.minWidth], but the remaining icon-only content still
/// did not fit below roughly 630px, which is a realistic width for DevTools
/// embedded in an IDE side panel. See
/// https://github.com/flutter/devtools/issues/4917.
const windowWidths = [1200.0, 800.0, 600.0, 500.0, 400.0];

const windowHeight = 800.0;

final fakeServiceConnection = FakeServiceConnectionManager();
final scriptManager = MockScriptManager();
mockConnectedApp(fakeServiceConnection.serviceManager.connectedApp!);
setGlobal(ServiceConnectionManager, fakeServiceConnection);
setGlobal(IdeTheme, IdeTheme());
setGlobal(ScriptManager, scriptManager);
setGlobal(NotificationService, NotificationService());
setGlobal(BreakpointManager, BreakpointManager());
setGlobal(
DevToolsEnvironmentParameters,
ExternalDevToolsEnvironmentParameters(),
);
setGlobal(PreferencesController, PreferencesController());
fakeServiceConnection.consoleService.ensureServiceInitialized();
when(
fakeServiceConnection.errorBadgeManager.errorCountNotifier('debugger'),
).thenReturn(ValueNotifier<int>(0));
final debuggerController = createMockDebuggerControllerWithDefaults();
Comment on lines +28 to +45

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

[CONCERN] Stateful mocks, controllers, and global services should be initialized inside a setUp block rather than at the top level of main(). When initialized at the top level of main(), they are shared across all tests in the file, which can lead to test pollution, side effects, and flakiness if one test modifies their state.

  late FakeServiceConnectionManager fakeServiceConnection;
  late MockScriptManager scriptManager;
  late MockDebuggerController debuggerController;

  setUp(() {
    fakeServiceConnection = FakeServiceConnectionManager();
    scriptManager = MockScriptManager();
    mockConnectedApp(fakeServiceConnection.serviceManager.connectedApp!);
    setGlobal(ServiceConnectionManager, fakeServiceConnection);
    setGlobal(IdeTheme, IdeTheme());
    setGlobal(ScriptManager, scriptManager);
    setGlobal(NotificationService, NotificationService());
    setGlobal(BreakpointManager, BreakpointManager());
    setGlobal(
      DevToolsEnvironmentParameters,
      ExternalDevToolsEnvironmentParameters(),
    );
    setGlobal(PreferencesController, PreferencesController());
    fakeServiceConnection.consoleService.ensureServiceInitialized();
    when(
      fakeServiceConnection.errorBadgeManager.errorCountNotifier('debugger'),
    ).thenReturn(ValueNotifier<int>(0));
    debuggerController = createMockDebuggerControllerWithDefaults();
  });
References
  1. Categorize Severity: Prefix every comment with a severity: [CONCERN] for maintainability issues. (link)


Future<void> pumpControls(WidgetTester tester) async {
await tester.pumpWidget(
wrapWithControllers(
const DebuggingControls(),
debugger: debuggerController,
),
);
await tester.pump();
}

group('DebuggingControls', () {
for (final width in windowWidths) {
testWidgetsWithWindowSize(
'does not overflow at ${width.toInt()}px',
Size(width, windowHeight),
(WidgetTester tester) async {
await pumpControls(tester);

expect(tester.takeException(), isNull);
},
);
}

testWidgetsWithWindowSize(
'keeps the file explorer button pinned to the right edge',
const Size(1200.0, windowHeight),
(WidgetTester tester) async {
await pumpControls(tester);

final controlsRight = tester
.getRect(find.byType(DebuggingControls))
.right;
final fileExplorerButtonRight = tester
.getRect(
find.widgetWithIcon(GaDevToolsButton, Icons.folder_outlined),
)
.right;

expect(fileExplorerButtonRight, equals(controlsRight));
},
);
});
}