Repository navigation
Description
Steps to reproduce
In Flutter web debug mode, a valid same-frame GlobalKey reparent that also removes a later sibling throws Bad state: Semantics node map was inconsistent after update. The reproduction uses only Flutter widgets and the SDK integration_test binding (no app code, custom RenderObjects, manually assigned node IDs or animations).
- In an ordinary Flutter project with
flutter_testandintegration_testSDK dev dependencies, save the code sample below asintegration_test/repro_test.dart. - Add
test_driver/integration_test.dart:import 'package:integration_test/integration_test_driver.dart'; Future<void> main() => integrationDriver();
- Start a ChromeDriver matching your Chrome on port 4444 (we used Chrome for Testing / ChromeDriver 155.0.8059.79), then run:
flutter drive -d web-server --headless --browser-name=chrome \ --driver-port=4444 --target=integration_test/repro_test.dart \ --driver=test_driver/integration_test.dart
- Controls: repeat with
--dart-define=REPRO_MODE=sibling-firstand--dart-define=REPRO_MODE=fresh-child. Add--profileto run without debug assertions.
Expected results
Moving the GlobalKey child to its new parent and removing its old sibling leaves a consistent engine semantics node map.
Actual results
Debug fails every time (2/2 fresh Chrome sessions); both controls pass; --profile passes (the consistency check is inside an assert). The framework tree changes from 0 → 1 → 2 → [3 retained, 4 removed] to 0 → 5 → 6 → 3, but the engine map keeps orphan node 4.
Suspected cause (source reading, not a verified bisect): in the web engine, SemanticsObject._visitDepthFirstInTraversalOrderCanSkipSubtree calls child!._visitDepthFirstInTraversalOrder(searchSubtree) (lib/_engine/engine/semantics/semantics.dart, around line 2651 in 3.47.5). That helper stops the whole traversal when the callback returns false, instead of skipping only that subtree. _finalizeTree returns false for a retained/reparented node, so later removed siblings at sufficient depth are never visited and stay in the map. That fits the sibling-first control passing. The pruning runs outside the assert, so profile/release builds likely keep the stale node too (only the check is debug-only). The code appears to come from #177069 (commit 16ae36ce20bbbe0543e70b2be5564f2dd595f06d, related issue #175180); this is a candidate, not a bisect result.
Code sample
Code sample
import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
// No Nebrivo imports, database, PDF, Riverpod, routes, or animation.
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('reparent a child while removing its old siblings', (tester) async {
final handle = tester.ensureSemantics();
final retainedKey = GlobalKey();
const mode = String.fromEnvironment('REPRO_MODE', defaultValue: 'reparent');
final changed = ValueNotifier(false);
Widget node(String label, Widget child, {Key? key}) => Semantics(
key: key, container: true, explicitChildNodes: true,
label: label, child: child,
);
try {
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: ValueListenableBuilder<bool>(
valueListenable: changed,
builder: (context, ready, _) {
final retained = node('retained', const SizedBox(width: 20, height: 20),
key: mode == 'fresh-child' ? const ValueKey('retained') : retainedKey);
final removed = node('removed sibling', const SizedBox(width: 20, height: 20));
return ready
? node('new outer', node('new middle', retained), key: const ValueKey('new'))
: node('old outer', node('old middle', Column(children: [
if (mode == 'sibling-first') removed,
retained,
if (mode != 'sibling-first') removed,
])), key: const ValueKey('old'));
},
),
));
debugPrint('REPRO: before reparent');
debugDumpSemanticsTree(DebugSemanticsDumpOrder.traversalOrder);
changed.value = true;
await tester.pump();
debugPrint('REPRO: after reparent');
debugDumpSemanticsTree(DebugSemanticsDumpOrder.traversalOrder);
// Leave any framework error with the binding so the driver records the
// original engine stack and fails. Never suppress a semantics failure.
expect(find.bySemanticsLabel('retained'), findsOneWidget);
} finally {
handle.dispose();
changed.dispose();
}
});
}Logs
Logs
══╡ EXCEPTION CAUGHT BY SCHEDULER LIBRARY ╞══════════════════════
The following StateError was thrown during a scheduler callback:
Bad state: Semantics node map was inconsistent after update:
BEFORE: The semantics node map is consistent:
Nodes in tree:
0: [1]
1: [2]
2: [3, 4]
3: []
4: []
Nodes in map: [0, 1, 2, 3, 4]
AFTER: The semantics node map is inconsistent:
Nodes in tree:
0: [5]
5: [6]
6: [3]
3: []
Nodes in map: [0, 3, 4, 5, 6]
When the exception was thrown, this was the stack:
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 274:3 throw_
lib/_engine/engine/semantics/semantics.dart 3281:9 <fn>
lib/_engine/engine/semantics/semantics.dart 3330:14 updateSemantics
lib/_engine/engine/window.dart 109:5 updateSemantics
package:flutter_test/src/window.dart 1276:5 updateSemantics
package:flutter/src/rendering/view.dart 384:5 updateSemantics
package:flutter/src/rendering/binding.dart 268:48 <fn>
package:flutter/src/semantics/semantics.dart 5078:22 sendSemanticsUpdate
package:flutter/src/rendering/object.dart 1651:7 flushSemantics
package:flutter/src/rendering/object.dart 1653:14 flushSemantics
package:flutter/src/rendering/binding.dart 699:7 drawFrame
package:flutter/src/widgets/binding.dart 1573:13 drawFrame
package:flutter/src/rendering/binding.dart 558:5 [_handlePersistentFrameCallback]Flutter Doctor output
Versions
- Flutter 3.47.5, framework
6a19cca56475dbfba1478ee68d7bd0c2ef891da1, engineaf7e796e161ae0bb1ff0758c71a7105418bd9ded, Dart 3.13.4: debug repro fails; sibling-first control and profile pass. - Flutter 3.47.7, framework
abaf9c5237, enginedeb287481e, Dart 3.13.5: debug repro fails; sibling-first and fresh-child controls pass. - Host macOS arm64; Chrome / ChromeDriver for Testing 155.0.8059.79, headless
web-serverdevice.
Activity
saurabh-mirajkar commentedon Oct 15, 2026
@Icosa2050 Thanks for the report. I reproduced this on Flutter
3.47.5and3.47.7on macOS with Chrome155.0.8059.41and ChromeDriver155.0.8059.79.In debug mode, the default
reparentcase throwsBad state: Semantics node map was inconsistent after updateon both versions. Thesibling-firstandfresh-childcontrol cases pass. In profile mode, all tests pass on both versions.Routing this to the team for further evaluation.
flutter-zl commentedon Oct 21, 2026
Demo 12: #194056, https://flutter-demo-12-before.web.app
The reporter's
MaterialApp.routersample, release build, lastFlutterErrorin the bottom bar.Null check operator used on a null valuefromdidChangeViewFocus. Expected: no error.--dart-define=WORKAROUND=trueand the error goes away.hannah-hyj commentedon Oct 21, 2026
From triage: we should look into if this is caused by framework sending a wrong semantics updates or if web side is handling he semantics updates wrongly.
[web] Do not stop pruning a detached semantics subtree at a reparente…