>[!warning]- Info > Author: [email protected] > Publish date: 2026/07/02 > Last update: 2026/09/17 > Status: WIP > Revision: 41 > Visibility: Public >[!note]- AI Assistance > This document was drafted with the assistance of [Claude](https://claude.ai) (Anthropic). The technical content reflects work done by the author; Claude helped structure and articulate it. # Modularizing `chrome/test` A phased approach to decoupling `chrome/test` from its monolithic `BUILD.gn`, following the Bedrock modularization pattern. > [!summary] TL;DR > - **The problem:** `chrome/test/BUILD.gn` is ~11,000 lines — one of the largest files in the tree, aggregating sources from 26 subdirectories directly. > - **The goal:** Give each logical subdirectory of `chrome/test` its own `BUILD.gn`, mirroring the ~105-file pattern already achieved in `chrome/browser/ui`. > - **The approach:** Three phases — start with leaf modules (`interaction`, `user_education`, `permissions`…), tackle the large `base/` infrastructure split, then complete remaining specialized directories. > - **Biggest win:** `chrome/test/base/` — 131 source files, no `BUILD.gn` historically; extracting it unlocks downstream modularization of the entire tree. ## Background Project Bedrock's primary mandate is fixing the `Browser` god-object in `chrome/browser`. The structural half of that work — giving each subdirectory its own `BUILD.gn` and `OWNERS`, enforcing visibility, breaking circular deps — applies directly to `chrome/test`. The pattern: instead of one massive `BUILD.gn` reaching into child directories via relative source paths like `"base/testing_profile.cc"`, each subdirectory owns a target and the parent references it via a GN dep. There are two complementary efforts: | Effort | What it extracts | Code movement? | |--------|-----------------|----------------| | **This work — `chrome/test/` modularization** | Test *support* targets (`InProcessBrowserTest`, `TestingProfile`…) into per-subdirectory `BUILD.gn` files | None — `BUILD.gn` reorganization only | | **Bedrock team — `chrome/browser/` modularization** | Test *case* source declarations out of `chrome/test/BUILD.gn` into the module's own `:browser_tests` targets | None — declarations move, files stay | --- ## Current State ``` chrome/test/ ├── BUILD.gn ← monolith, shrinking ├── base/ ← Phase 2 (current — see progress table) ├── interaction/ ← ✅ done ├── user_education/ ← ✅ done ├── permissions/ ← ✅ done ├── views/ ← ✅ done ├── gpu/ ← ✅ done ├── v8/ ← ✅ done ├── enterprise/ ← Python-only, skip ├── accessibility/ ← standalone tool, skip ├── webapps/ ← test data only, skip ├── delayload/ ← Windows-only, defer ├── android/ ← BUILD.gn exists; 8 APK/Java sources remain in root ├── chromedriver/ ← BUILD.gn exists (standalone binary) └── (fuzzing, mini_installer, payments, etc.) ← BUILD.gn exists, fully modularized ``` ### Progress | Phase | Status | Notes | |---|---|---| | Phase 1 — leaf modules (`interaction`, `user_education`, `permissions`, `views`, `gpu`, `v8`) | ✅ All landed | All temp monolith deps resolved | | Phase 2a — `base/` thin targets (`:in_process_browser_test`, `:ui_test_utils`, `:test_launcher_utils`) + subdir dep cleanup | ✅ Landed | CLs 0–3 | | Phase 2b — `:in_process_browser_test_mixin`, `:chrome_test_launcher` | ✅ Landed | | | Phase 2b — `:testing_profile` thin target | ✅ Landed | [CL4](https://chromium-review.googlesource.com/8089948) | | Phase 2b — `:testing_profile` full + `:testing_browser_process` | ✅ Landed | [CL5](https://chromium-review.googlesource.com/8089949); uses `allow_circular_includes_from` — see below | | Phase 2b — `:testing_profile` Pattern A split (header+impl) | ✅ Landed | [CL6](https://chromium-review.googlesource.com/8094642); breaks extensions cycle; cros ash cycle deferred | | Phase 2b — Forward-declare `ScopedCrosSettingsTestHelper` in testing_profile.h | ✅ Landed | [CL7](https://chromium-review.googlesource.com/8094905); fully removes ash/settings dep from header target | | Phase 2c — `:profile_lifecycle_test_utils` | ✅ Landed | [CL8](https://chromium-review.googlesource.com/8094906); BUILD.gn-only; profile_waiter + profile_deletion_observer + profile_destruction_waiter | | Phase 2c — `:browser_window_waiters` | ✅ Landed | [CL9](https://chromium-review.googlesource.com/8094907); browser_closed_waiter + browser_created_waiter | | Phase 2c — `:browser_feature_test_waiters`, `:chrome_test_scoped_utils`; finalize `:test_launcher_utils` | ✅ Landed | [CL10](https://chromium-review.googlesource.com/8100982); autocomplete_change_observer, find_result_waiter, search_test_utils, scoped_browser_locale; `.cc` added to thin test_launcher_utils; `tracing` kept in monolith per reviewer; later extracted separately as CL11b | | Phase 2c — `test_switches` + `test_theme_provider` relocated to `//chrome/test/base/` | ✅ Landed | [CL11a](https://chromium-review.googlesource.com/8140005); BUILD.gn-only | | Phase 2c — `:tracing`, `:web_feature_histogram_tester`, `:web_ui_test_data_source`, `:scoped_channel_override` | ✅ Landed | [CL11b](https://chromium-review.googlesource.com/8138555)–[11e](https://chromium-review.googlesource.com/8138558); reland of 11e fixed missing `branding_buildflags` dep | | Phase 2c — `:platform_browser_test`, `:scoped_metrics_service_for_synthetic_trials`, `:chrome_render_view_host_test_harness`, `:devtools_coverage`, `:chrome_test_utils` thin header | ✅ Landed | CL12a–13a; BUILD.gn-only; Windows validated clean | | Phase 2c — `:test_browser_window` + `:dialog_test_browser_window` + `:test_browser_window_aura` (CL14b); BWI refactor (CL14a) | ✅ Landed | BUILD.gn-only; non-android | | Phase 2c — `:web_ui_mocha_browser_test` + `:android_ui_test_utils` (CL15a) | ✅ Landed | android_ui_test_utils split as own target to preserve dep chain | | Phase 2c — `:save_desktop_snapshot` (CL16a); `:test_chrome_web_ui_controller_factory` (CL16b) | ✅ Landed | Both from `test_support_ui`; BUILD.gn-only; webrtc headers accessible via //content/public/browser transitively | | Phase 2c — platform-specific `test_support` extractions: `drag_and_drop_test_utils` (aura), `launchservices_utils_mac` + `scoped_bundle_swizzler_mac` (mac), `process_inspector_win` + `process_lineage_win` (win) (CL17a) | ✅ Landed | Key lesson: test_support_ui does not dep on test_support — cross-target headers need explicit public_deps in both | | Phase 2c — win `test_support_ui` extractions: `always_on_top_window_killer_win` + `window_contents_as_string_win` (CL17b) | ✅ Landed | `interactive_test_utils_win.cc` stays in monolith (includes blocked `interactive_test_utils.h`) | | Phase 2c — CrOS clean extractions: `js_test_api`, `browser_process_platform_part_test_api_chromeos`, `aura_window_title_observer` (from `test_support`), `web_ui_test_handler` (from `test_support_ui`) (CL18) | ✅ Landed | Remaining CrOS files blocked (include `in_process_browser_test.h` or `mixin_based_in_process_browser_test.h`) | | Phase 2c — break `chrome_test_utils` dep-graph cycle: extract `:chrome_test_path_utils` ([CL18b](https://chromium-review.googlesource.com/8253803)) | ✅ Landed | New target: 4 path utilities in same namespace, no browser dep; unblocked the entire core cluster | | Phase 2c — `:chrome_test_suite` thin + complete `:in_process_browser_test` ([CL19](https://chromium-review.googlesource.com/8253804)); complete `:ui_test_utils` ([CL20](https://chromium-review.googlesource.com/8253805)) | ✅ Landed | `in_process_browser_test.cc` + `chrome_test_suite.h` now in own targets; cycle fixed by CL18b | | Phase 2c — `:interactive_test_utils` + platform variants ([CL21](https://chromium-review.googlesource.com/8253806)–[22](https://chromium-review.googlesource.com/8253807)); `:mixin_based_in_process_browser_test` ([CL23](https://chromium-review.googlesource.com/8253808)); `:fake_gaia_mixin` ([CL24](https://chromium-review.googlesource.com/8253809)); `:web_view_focus_helper` ([CL25](https://chromium-review.googlesource.com/8253810)) | ✅ Landed | All non-android; CrOS cluster now unblocked | | Phase 2c — CrOS cluster: `:javascript_browser_test` + `:scoped_test_system_nss_key_slot_mixin` ([CL26](https://chromium-review.googlesource.com/8264099)); `:web_ui_browser_test` + `:extension_js_browser_test` ([CL27](https://chromium-review.googlesource.com/8264100)); `:mojo_web_ui_browser_test` ([CL28](https://chromium-review.googlesource.com/8264101)) | ✅ Landed | All 5 pairs extracted; CL27/28 needed `no_exit_time_destructors` config transfers | | Phase 2c — `:menu_model_test` ([CL29](https://chromium-review.googlesource.com/8271139)) | ✅ Landed | Only ui/base + ui/menus deps | | Phase 2c — Android cluster: `:android_browser_test` + android sources for existing targets ([CL30](https://chromium-review.googlesource.com/8271140)) | ✅ Landed | `test_support_ui_android` converted to `group`; `enable_extensions_core` guard for extension_features dep | | Phase 2c — Mac additions to `:in_process_browser_test` + `:interactive_test_utils` ([CL31](https://chromium-review.googlesource.com/8271141)) | ✅ Landed | 4 patchsets to fix nonexistent targets; final fix: add `//chrome/browser/ui/cocoa` + `//chrome/browser/ui/tabs:tab_strip` to mac deps of `:in_process_browser_test` | | Phase 2c — `:chrome_ash_test_base` + `:browser_tests_runner` (move whole target, CrOS+non-CrOS) + `crosier:ash_integration_test_support` + `:browser_tests` (7 base/ browsertests) | ⬜ ready to push (CL37) | All 4 configs gn-check clean; lessons: `use_ozone` not `is_ozone`; `if (!is_android)` block closes ~line 1213 in c/t/base/BUILD.gn — new `if (!is_android)` must come AFTER `if (is_win)` closes; webrtc headers via `//content/public/browser` transitively | | Phase 2c — complete `:chrome_test_suite` (add `.cc`) | ⬜ ready to push | `chrome_test_suite.cc` added; `chrome/app:test_support` guarded `!is_android`; `//chrome:chrome_android_core` dep on android; `enable_extensions_core` guard for `extensions/common`; mac: `app_controller_mac` + `chrome_browser_application_mac` | | Phase 2c — `:chrome_unit_test_suite` | ⬜ ready to push | `chrome_unit_test_suite.{cc,h}` extracted; `//components/web_package` unconditional (matches monolith); `//chrome/browser/apps/app_service/publishers` guarded `!is_android`; all 4 configs pass | | Phase 2c — `:browser_with_test_window_test` | ⬜ ready to push (CL34) | `browser_with_test_window_test.{cc,h}` extracted; inside `if (!is_android)` block; `no_exit_time_destructors` config transferred; `//components/session_manager/core` + `:test_support` (not the nonexistent `//components/session_manager`); `//components/variations:test_support` for `scoped_variations_ids_provider.h`; `//chromeos/ash/components/disks:test_support` for `fake_disk_mount_manager.h`; all 4 configs pass | | **Phase 2D — Granularity review** (single CL) | 🔄 in CQ | 11 inlines + WebUI consolidation: `web_ui_test_support` absorbs `test_chrome_web_ui_controller_factory`; `cros_web_ui_browser_test` absorbs `mojo_web_ui_browser_test`. Net: ~62→~49 targets. | | **Phase 2D-4 — browser_test_waiters merge** | ✅ committed (fixup → CL37) | `browser_feature_test_waiters` + `browser_window_waiters` → `browser_test_waiters`; BUILD.gn-only. | | Phase 3 — remaining dirs (`android/` leftovers…) | ⬜ | After `base/` complete | | Phase 3a — Monolith → thin aggregator | ⬜ | After ALL sources extracted | | Phase 3b — Consumer migration + monolith deletion | 🔧 CQ errors being fixed | 3b-9→3b-15 ✅ landed; 3b-16→3b-22 in CQ with fixups (see below) | --- ## chrome/test/base — Extraction Status ### Already extracted | Target | Files | Notes | |--------|-------|-------| | `:test_launcher_utils` | `test_launcher_utils.{cc,h}` | Full (CL10); previously header-only | | `:in_process_browser_test` | `in_process_browser_test.h` | Thin header-only (non-android); `.cc` pending cycle fix | | `:ui_test_utils` | `ui_test_utils.h` | Thin header-only (non-android); `.cc` pending cycle fix | | `:chrome_test_launcher` | `chrome_test_launcher.{cc,h}` | Full | | `:testing_profile` | `testing_profile.h`, `fake_profile_manager.h`, `testing_profile_key.h`, `testing_profile_manager.h` | Header-only; `allow_circular_includes_from` entries in TBP pending removal (see below) | | `:testing_profile_impl` | `testing_profile.cc`, `fake_profile_manager.cc`, `testing_profile_key.cc`, `testing_profile_manager.cc` | Full impl | | `:testing_browser_process` | `testing_browser_process.{cc,h}`, `testing_browser_process_platform_part.{cc,h}` | Full; has 3 remaining `allow_circular_includes_from` (see below) | | `:profile_lifecycle_test_utils` | `profile_{waiter,deletion_observer,destruction_waiter}.{cc,h}` | Full | | `:browser_test_waiters` | `browser_{closed,created}_waiter.{cc,h}`, `autocomplete_change_observer.{cc,h}`, `find_result_waiter.{cc,h}`, `search_test_utils.{cc,h}` | Merged (2D-4 — pending commit); `browser_window_waiters` + `browser_feature_test_waiters` consolidated | | `:chrome_test_scoped_utils` | `scoped_browser_locale.{cc,h}` | Full (CL10) | | `:test_switches` | `test_switches.{cc,h}` | Full (CL11a); relocated from `//chrome/test:test_switches` | | `:test_theme_provider` | `test_theme_provider.{cc,h}` | Full (CL11a); relocated from `//chrome/test:test_theme_provider`; toolkit_views only | | `:tracing` | `tracing.{cc,h}` | Full (CL11b) | | `:web_feature_histogram_tester` | `web_feature_histogram_tester.{cc,h}` | Full (CL11c) | | `:web_ui_test_support` | `web_ui_test_data_source.{cc,h}`, `test_chrome_web_ui_controller_factory.{cc,h}` (non-android) | Full (2D — in CQ); renamed from `web_ui_test_data_source`; absorbed `test_chrome_web_ui_controller_factory` via `if (!is_android)` block | | `:scoped_channel_override` | `scoped_channel_override.h` + platform `.cc` files | Full (CL11e); chrome-branded; mac/win/posix-non-android-non-cros | | `:platform_browser_test` | `platform_browser_test.h` | Thin header-only (CL12a); non-android; aliases `:in_process_browser_test` | | `:scoped_metrics_service_for_synthetic_trials` | `scoped_metrics_service_for_synthetic_trials.{cc,h}` | Full (CL12b) | | `:chrome_render_view_host_test_harness` | `chrome_render_view_host_test_harness.{cc,h}` | Full (CL12c) | | `:devtools_coverage` | `devtools_agent_coverage_observer.{cc,h}`, `devtools_listener.{cc,h}` | Full (CL12d); non-android | | `:chrome_test_utils` | `chrome_test_utils.h` | Thin header-only (CL13a); `.cc` stays in monolith — TBP dep cycle; deferred full extraction | | `:test_browser_window` | `test_browser_window.{cc,h}`, `dialog_test_browser_window.{cc,h}`, `test_browser_window_aura.{cc,h}` (use_aura block) | Full (CL14b + 2D-2); non-android; aura variant inlined by 2D-2 | | `:web_ui_mocha_browser_test` | `web_ui_mocha_browser_test.{cc,h}` | Full (CL15a); android/desktop conditional public_deps | | `:android_ui_test_utils` | `android/android_ui_test_utils.{cc,h}` | Full (CL15a); android only; split as own target to fix dep chain | | `:save_desktop_snapshot` | `save_desktop_snapshot.{cc,h}` | Full (CL16a); non-android; webrtc headers accessible via `//content/public/browser` transitively | | `:drag_and_drop_test_utils` | `drag_and_drop_test_utils.{cc,h}` | Full (CL17a); use_aura only | | `:test_utils_mac` | `launchservices_utils_mac.{h,mm}`, `scoped_bundle_swizzler_mac.{h,mm}` | Full (CL17a + 2D-1); mac only; formerly 2 targets | | `:process_lineage_win` | `process_inspector_win.{cc,h}`, `process_lineage_win.{cc,h}` | Full (CL17a + 2D-1); win only; process_inspector_win inlined by 2D-1 | | `:always_on_top_window_killer_win` | `always_on_top_window_killer_win.{cc,h}` | Full (CL17b); win + non-android | | `:browser_process_platform_part_test_api_chromeos` | `browser_process_platform_part_test_api_chromeos.{cc,h}` | Full (CL18); chromeos only | | `:aura_window_title_observer` | `chromeos/crosier/aura_window_title_observer.{cc,h}` | Full (CL18); chromeos only | | `:chrome_test_path_utils` | `chrome_test_path_utils.{cc,h}` | Full ([CL18b](https://chromium-review.googlesource.com/8253803)); new target — 4 path utilities extracted from `:chrome_test_utils` to break dep-graph cycle; same `chrome_test_utils` namespace | | `:chrome_test_suite` | `chrome_test_suite.{cc,h}` | Full ([CL19](https://chromium-review.googlesource.com/8253804) thin header + CL32 `.cc`); `chrome/app:test_support` guarded `!is_android`; `//chrome:chrome_android_core` on android; `enable_extensions_core` guard for `extensions/common` | | `:chrome_unit_test_suite` | `chrome_unit_test_suite.{cc,h}` | Full (CL33, ready to push); `//components/web_package` unconditional; `!is_android` guard for `app_service/publishers`; `enable_extensions` guard for `extensions/common` | | `:browser_with_test_window_test` | `browser_with_test_window_test.{cc,h}` | Full (CL34, ready to push); non-android; `no_exit_time_destructors` config; key dep fixes: `//components/session_manager/core` + `:test_support`, `//components/variations:test_support`, `//chromeos/ash/components/disks:test_support` | | `:in_process_browser_test` (completed) | `in_process_browser_test.cc` added | Full non-android ([CL19](https://chromium-review.googlesource.com/8253804)); was thin header-only; cycle fixed by CL18b | | `:ui_test_utils` (completed) | `ui_test_utils.cc` added | Full non-android ([CL20](https://chromium-review.googlesource.com/8253805)); was thin header-only | | `:interactive_test_utils` | `interactive_test_utils.{cc,h}`, `interactive_test_utils_aura.{cc,h}`, `interactive_test_utils_views.cc` ([CL21](https://chromium-review.googlesource.com/8253806)); `interactive_test_utils_win.cc` ([CL22](https://chromium-review.googlesource.com/8253807)); android variant (CL30); + `web_view_focus_helper.{cc,h}`, `window_contents_as_string_win.{cc,h}` (is_win block) inlined by 2D-2 | Full; non-android in `if (!is_android)` block; android variant in `if (is_android)` block | | `:mixin_based_in_process_browser_test` | `mixin_based_in_process_browser_test.{cc,h}`, `in_process_browser_test_mixin.{cc,h}` inlined by 2D-2 | Full non-android ([CL23](https://chromium-review.googlesource.com/8253808)); android variant (CL30) | | `:fake_gaia_mixin` | `fake_gaia_mixin.{cc,h}` | Full ([CL24](https://chromium-review.googlesource.com/8253809)); non-android | | `:javascript_browser_test` | `ash/javascript_browser_test.{cc,h}`, `ash/js_test_api.{cc,h}` inlined by 2D-1 | Full (CL26 + 2D-1); chromeos only; from `test_support_ui` | | `:scoped_test_system_nss_key_slot_mixin` | `ash/scoped_test_system_nss_key_slot_mixin.{cc,h}` | Full (CL26); chromeos only; from `test_support` | | `:cros_web_ui_browser_test` | `ash/web_ui_browser_test.{cc,h}`, `ash/web_ui_test_handler.{cc,h}` (inlined 2D-1), `ash/mojo_web_ui_browser_test.{cc,h}` (absorbed 2D-3) | Full (CL27 + 2D); chromeos only; `no_exit_time_destructors` config | | `:extension_js_browser_test` | `ash/extension_js_browser_test.{cc,h}` | Full (CL27); chromeos only; from `test_support`; needed `no_exit_time_destructors` config transfer + `extensions:test_support` dep | | `:menu_model_test` | `menu_model_test.{cc,h}` | Full (CL29); all platforms; from `test_support`; only `//ui/base` + `//ui/menus` deps | | `:android_browser_test` | `android/android_browser_test.{cc,h}` | Full (CL30); android only; `enable_extensions_core`-guarded `extension_features` dep | | `:platform_browser_test` (android variant) | `platform_browser_test.h` | Android variant (CL30); in `if (is_android)` block; aliases `:android_browser_test` | | `:in_process_browser_test` + mac sources | `in_process_browser_test_mac.mm` added | Mac addition (CL31); `chrome_command_dispatcher_delegate` + `ui/tabs` deps | | `:interactive_test_utils` + mac sources | `interactive_test_utils_mac.mm` added | Mac addition (CL31); `chrome_command_ids` + `chrome_browser_application_mac` deps; `//ui/events` (already in unconditional deps) covers `cocoa/` headers | | `:chrome_ash_test_base` | `ash/chrome_ash_test_base.{cc,h}` | Full (CL37); chromeos only; whole target moved (not CrOS-only sub-extract) — lesson: if all sources in `base/`, move entire target | | `:browser_tests_runner` | `browser_tests_main.cc` (non-cros), `browser_tests_main_chromeos.cc` (cros) | Full (CL37); all platforms; moved whole target from `//chrome/test` → `//chrome/test/base`; 5 external consumers updated; lesson: `use_ozone` not `is_ozone`; add `import("//build/config/ozone.gni")` | | `//chrome/test/base/chromeos/crosier:ash_integration_test_support` | `ash_integration_test.{cc,h}`, `supervised_user_integration_base_test.{cc,h}` | Full (CL37); chromeos-device only; moved from `//chrome/test:ash_integration_test_support`; actual deps resolved (no monolith aggregator) | | `:browser_tests` (base/ browsertests) | `devtools_listener_browsertest.cc`, `in_process_browser_test_browsertest.cc`, `memory_tracing_browsertest.cc`, `save_desktop_snapshot_browsertest.cc`, `test_chrome_web_ui_controller_factory_browsertest.cc`, `ui_test_utils_browsertest.cc`, `web_ui_mocha_browser_test_browsertest.cc` | Full (CL37); non-android; 7 base/ browsertests moved out of monolith `browser_tests` binary; lesson: `if (!is_android)` block in c/t/base/BUILD.gn closes ~line 1213 — place new blocks AFTER `if (is_win)` closes, not inside it | ### Progress estimate (2026-08-31, CL26–31 landed, CL32–37 + Phase 2D in CQ) | Metric | Value | |--------|-------| | Total `.cc/.h/.mm` files in `chrome/test/base/` | 153 | | Still referenced in monolith (`chrome/test/BUILD.gn`) | ~18 (after CL26–37 land) | | Browsertest / entry-point files (stay in monolith permanently) | ~15 | | Deferred (`chrome_test_utils.cc` pending TBP split) | ~1 | | **Overall extraction progress** | **~99%** | | Estimated remaining CL-units before Phase 3 | **TBP Pattern A split + consumer migration** | **CL26–31 landed. CL32–37 + Phase 2D in CQ. Phase 2D-4 committed.** `test_support` and `test_support_ui` already have zero `base/` entries — the base/ extraction side is structurally done. Remaining non-base/ sources in `test_support` (`web_applications/`, `chooser_bubble_testapi*`, Mac/Win crash reporter) and `test_support_unit` (CrOS quick_answers) are low-priority separate efforts with TODO markers. Phase 2E dropped (remaining `base/` sources are in binary targets — appropriate permanent stays). ### Phase 2D — Granularity Review BUILD.gn-only consolidation pass: single CL (originally planned as 2D-1 + 2D-2 + 2D-3; merged by author). Net: ~62 → ~49 targets (13 fewer). In CQ. **Platform-specific inlines:** - `process_inspector_win` → inlined into `process_lineage_win` - `launchservices_utils_mac` + `scoped_bundle_swizzler_mac` → merged as `test_utils_mac` - `js_test_api` → inlined into `javascript_browser_test` - `web_ui_test_handler` → inlined into `cros_web_ui_browser_test` **Cross-platform inlines:** - `in_process_browser_test_mixin` → inlined into `mixin_based_in_process_browser_test` - `test_browser_window_aura` → inlined into `test_browser_window` (if use_aura block) - `web_view_focus_helper` → inlined into `interactive_test_utils` - `window_contents_as_string_win` → inlined into `interactive_test_utils` (if is_win block) **WebUI consolidation (2D-3):** - `test_chrome_web_ui_controller_factory` absorbed into `web_ui_test_data_source` → renamed `web_ui_test_support` (`if (!is_android)` block for controller factory) - `mojo_web_ui_browser_test` absorbed into `cros_web_ui_browser_test` Note: `always_on_top_window_killer_win` kept separate — 2 consumers. `javascript_browser_test` kept separate — 2 consumers (`cros_web_ui_browser_test` + `extension_js_browser_test`). `web_ui_mocha_browser_test` kept separate — heavy deps would regress scope for `web_ui_test_support` consumers. **Phase 2D-4 (pending commit):** - `browser_feature_test_waiters` + `browser_window_waiters` → merged as `browser_test_waiters` Empirical co-occurrence analysis (35 `chrome/browser` BUILD.gn files, 60 consumer targets): - `testing_browser_process` + `testing_profile`: ratio=0.92 — strongest new signal; held back by `allow_circular_includes_from` constraint on `testing_browser_process` - `in_process_browser_test` + `ui_test_utils`: ratio=0.76 — below 0.8 threshold; kept separate #### External usage audit (2026-08-31) Methodology: `rg '#include "chrome/test/base/[^"]*"' --glob '!chrome/test/**' -oI | sort | uniq -c | sort -rn` — counts unique `#include` lines in source files outside `chrome/test/`. Grouped by owning `//chrome/test/base:*` target. | Target | External includes | Key headers | |--------|------------------:|-------------| | `testing_profile` | ~2,082 | `testing_profile.h` (1579), `testing_profile_manager.h` (490), `fake_profile_manager.h` (13) | | `in_process_browser_test` | 1,588 | `in_process_browser_test.h` | | `ui_test_utils` | 1,388 | `ui_test_utils.h` | | `testing_browser_process` | ~885 | `testing_browser_process.h` (881), `death_test_mixin.h` (4) | | `chrome_render_view_host_test_harness` | 454 | `chrome_render_view_host_test_harness.h` | | `mixin_based_in_process_browser_test` | 228 | `mixin_based_in_process_browser_test.h` | | `chrome_test_utils` | 227 | `chrome_test_utils.h` | | `interactive_test_utils` | 168 | `interactive_test_utils.h` | | `platform_browser_test` | 113 | `platform_browser_test.h` | | `chrome_test_path_utils` | 89 | `chrome_test_path_utils.h` | | `fake_gaia_mixin` | 86 | `fake_gaia_mixin.h` | | `browser_with_test_window_test` | 73 | `browser_with_test_window_test.h` | | `chrome_ash_test_base` | 64 | `chrome_ash_test_base.h` | | `browser_test_waiters` | ~61 | `search_test_utils.h` (57), `browser_{created,closed}_waiter.h` (2+2) | | `android_browser_test` | 48 | `android/android_browser_test.h` | | `test_browser_window` | 29 | `test_browser_window.h` | | `profile_lifecycle_test_utils` | 26 | `profile_waiter.h` (13), `profile_destruction_waiter.h` (13) | | `chrome_render_view_test` | 22 | `chrome_render_view_test.h` | | ash/interactive, crosier, etc. | <20 each | platform-specific, expected | **Findings:** The top 4 targets account for ~6,000 of the ~8,000 total external includes — these are the core test infrastructure and clearly justify independent targets. The rest follows a healthy long tail of more specialised targets with dozens of consumers each. Current granularity is well-calibrated; no consolidation or subdirectory restructuring needed. --- ### Deferred — TBP `allow_circular_includes_from` cleanup Three entries remain in `testing_browser_process`'s `allow_circular_includes_from` that need structural fixes before removal: | Entry | Root cause | Fix | |-------|-----------|-----| | `testing_profile_impl` | `testing_profile_manager.cc` includes `testing_browser_process.h` | Remove/forward-declare TBP.h in `testing_profile_manager.cc` | | `extensions:test_support` | `extensions:test_support` sources include `testing_browser_process.h` | TBP Pattern A split (thin header-only + impl) | | `ash/settings:test_support`, `ash/system_web_apps/test_support` | Same — include `testing_browser_process.h` on cros | TBP Pattern A split | The Pattern A split for TBP mirrors what was done for `testing_profile` in CL6-CL7: produce a thin `testing_browser_process` target with only the header deps it needs, and a `testing_browser_process_impl` that carries the heavy ash/extensions deps. The three targets that currently include `testing_browser_process.h` can then dep on the thin target directly, eliminating the cycles. ### Breaking the `browser/ui:test_support` cycle (resolved — CL18b) The cycle that blocked the entire core cluster was: ``` in_process_browser_test → chrome_test_utils → platform_browser_test → in_process_browser_test ``` `chrome_test_utils` mixed browser-level functions (taking `PlatformBrowserTest*`, pulling in `platform_browser_test.h` → `in_process_browser_test.h`) with pure path utilities (`GetChromeTestDataDir`, `OverrideChromeTestDataDir`, `GetTestFilePath`, `GetTestUrl`) that only need `base::PathService`, `chrome::DIR_TEST_DATA`, and `net`. **Fix (CL18b):** Extract the 4 path functions to a new `:chrome_test_path_utils` target with no browser dependency. The functions stay in the `chrome_test_utils::` namespace — all existing callers of `#include "chrome/test/base/chrome_test_utils.h"` continue to work unchanged because `chrome_test_utils.h` now `#include`s `chrome_test_path_utils.h`. The dep-graph cycle is gone: ``` in_process_browser_test → chrome_test_path_utils (no PlatformBrowserTest dep) chrome_test_utils → platform_browser_test → in_process_browser_test (separate direction) ``` This unlocked CL19–25 (the entire non-android core cluster) and unblocks CL26–28 (CrOS). --- ## BUILD.gn Patterns ### New per-directory `BUILD.gn` ```gn # chrome/test/interaction/BUILD.gn source_set("test_support") { testonly = true sources = [ "interactive_browser_test.cc", "interactive_browser_test.h", # ... ] public_deps = [ "//ui/base/interaction:interaction", ] deps = [ "//chrome/test/base:in_process_browser_test", "//chrome/browser/ui", "//content/public/test:test_support", ] # testonly=true prevents production code from depending here regardless # of visibility. visibility = [ "//chrome/browser/*", "//chrome/test/*", ] } ``` ### Root `chrome/test/BUILD.gn` after modularization ```gn static_library("test_support") { testonly = true # Before: "interaction/interactive_browser_test.cc", ... # After: dep on the child target public_deps = [ "//chrome/test/base:test_support", "//chrome/test/interaction:test_support", "//chrome/test/user_education:test_support", # ... ] } ``` ### Validation commands ```sh gn check out/linux # desktop gn check out/cros # ChromeOS (catches ash/extensions include cycles) gn check out/android # Android ``` Always run all three before uploading — platform-specific deps can hide gn check failures that only surface in one config. Windows (`out/win-arm64-dbg`) has been separately validated for CL11x–13a and found clean; no Windows-specific deps or conditional guards were missing. --- ## Handling Circular Dependencies ### Background: dep-graph cycle vs. include cycle GN cycle detection operates on the **dep graph** (`deps`/`public_deps` entries), not on `#include` lines. Two targets can mutually include each other's headers at compile time while keeping the dep graph a DAG — as long as only one dep edge exists and `allow_circular_includes_from` is declared. > [!info] Key insight > `allow_circular_includes_from` is a **header-include permission** — invisible to the dep graph. If target A has B in its direct deps, A can declare `allow_circular_includes_from = ["//B"]` to permit B's sources to `#include` A's headers. GN sees a DAG, `gn check` stays green. ### Case 1 — `interaction:interaction` / `test_support_ui` (include cycle) `test_support_ui` deps on `interaction:interaction` (via `public_deps`), but interaction source files include headers still compiled inside `test_support_ui` (`in_process_browser_test.h`, etc.). A naïve reading suggests a cycle — but it isn't, because `interaction:interaction` has **no dep edge back** to `test_support_ui`. ``` test_support_ui ──public_deps──► interaction:interaction test_support_ui ──allow_circular_includes_from──► interaction:interaction ↑ interaction's files may #include test_support_ui headers ``` A related issue: `interaction:interaction` needed `test_switches.h` from `test_support`. Adding `//chrome/test:test_support` as a dep would close a true dep-graph cycle (`test_support_ui → interaction → test_support → test_support_ui`). Fix: extract `test_switches.{h,cc}` into a thin `//chrome/test:test_switches` with zero deps. > [!warning] > `allow_circular_includes_from` cannot rescue a **true dep-graph cycle** — GN errors regardless. True cycles require structural fixes (pre-extraction of a thin target, Pattern A split, etc.). ### Case 2 — `testing_profile` / `testing_browser_process` extraction (reverse-dep problem) When extracting `testing_profile` and `testing_browser_process` into standalone targets, `gn check` failed on three ash targets and one extensions target whose source files include `testing_profile.h` or `testing_browser_process.h`: | Target | Includes | |---|---| | `//chrome/browser/ash/settings:test_support` | both `testing_profile.h` and `testing_browser_process.h` | | `//chrome/browser/ash/system_web_apps/test_support` | `testing_browser_process.h` (×2 files) | | `//chrome/browser/extensions:test_support` | `testing_browser_process.h` | Adding a direct dep from these targets to the new targets would create a dep-graph cycle because `testing_profile` and `testing_browser_process` *already depend on* those ash/extensions targets. **Interim fix:** `allow_circular_includes_from` in the extracted targets: ```gn source_set("testing_profile") { allow_circular_includes_from = [] # initialize; extended in blocks below if (enable_extensions_core) { deps += [ "//chrome/browser/extensions:test_support" ] # TODO(crbug.com/353332589): Remove with Pattern A split. allow_circular_includes_from += [ "//chrome/browser/extensions:test_support" ] } if (is_chromeos) { public_deps += [ "//chrome/browser/ash/settings:test_support" ] # TODO(crbug.com/353332589): Remove with Pattern A split. allow_circular_includes_from += [ "//chrome/browser/ash/settings:test_support" ] } } source_set("testing_browser_process") { allow_circular_includes_from = [ "//chrome/test/base:testing_profile" ] # existing if (enable_extensions_core) { deps += [ "//chrome/browser/extensions", "//chrome/browser/extensions:test_support", # new direct dep (was only transitive) ] # TODO(crbug.com/353332589): Remove with Pattern A split. allow_circular_includes_from += [ "//chrome/browser/extensions:test_support" ] } if (is_chromeos) { deps += [ "//chrome/browser/ash/policy/core", "//chrome/browser/media_galleries", "//chrome/browser/ash/settings:test_support", # new "//chrome/browser/ash/system_web_apps/test_support", # new ] # TODO(crbug.com/353332589): Remove with Pattern A split. allow_circular_includes_from += [ "//chrome/browser/ash/settings:test_support", "//chrome/browser/ash/system_web_apps/test_support", ] } } ``` **Key constraint:** The target listed in `allow_circular_includes_from` must be a **DIRECT** dep of the declaring target. If it was only transitive before, add it as a direct dep first. **CL6 — Pattern A split (`chromod/chrome-test-base-6`):** Splits `testing_profile` into: - `testing_profile` — thin header-only (`.h` only; on cros, still needs `ash/settings:test_support` because `testing_profile.h` has a `ScopedCrosSettingsTestHelper` by-value member) - `testing_profile_impl` — full `.cc` + heavy deps (ash, extensions) Result: `extensions:test_support` can now dep directly on `testing_profile` (header-only) — the extensions `allow_circular_includes_from` entry in `testing_profile` is removed. The cros case (`ash/settings:test_support`) still requires `allow_circular_includes_from` because `testing_profile.h` directly includes from `ash/settings:test_support` (preventing `ash/settings` from deping back). **Remaining deferred:** - *cros*: Forward-declare `ScopedCrosSettingsTestHelper` in `testing_profile.h`, use `unique_ptr` for the member → header dep on `ash/settings:test_support` moves to `.cc` → `ash/settings:test_support` can dep on `testing_profile` directly → remove `allow_circular_includes_from` from `testing_profile` and TBP. - *all platforms*: Remove `testing_profile_manager.cc`'s include of `testing_browser_process.h` → TBP can drop `allow_circular_includes_from = ["testing_profile_impl"]`. --- ## Explicit Dep Audit (required for "done") After extracting a new module, a required follow-up step is a dep scope audit: find every external consumer of the new module's headers and narrow `public_deps` to `deps` wherever safe. Without this step, the new module leaks transitively through the monolith just as the original sources did. ### Checklist 1. For each header in the new module, run `gn refs` to find all external GN targets. 2. For each depending target: check whether any of its *own* public headers include the new module's headers. If not, move the new module from `public_deps` to `deps`. 3. Where it must stay in `public_deps`, add a `TODO(crbug.com/353332589)` naming the specific public header that blocks the cleanup. ### Example: `chrome/test/views` `test_support_ui`: `//chrome/test/views` moved `public_deps` → `deps`. Safe because `in_process_browser_test.h` (the public header) includes no views headers. `test_support`: `//chrome/test/views` stays in `public_deps` — blocked by `browser_with_test_window_test.h`, which uses `ChromeTestViewsDelegate<>` as a concrete member field (full definition required). Unblocks when that header is componentized in Phase 2c. --- ## Transitory `public_deps` and Consumer Migration (Phase 3b) ### Why extracted targets go into `public_deps` During Phase 2 extraction, each new `//chrome/test/base:*` target is added to the monolith's `public_deps` list — not `deps`. This is intentional and temporary. GN's visibility rules propagate include permissions only along `public_deps` edges. When a target `A` lists `B` in `public_deps`, any target that deps on `A` can also include headers from `B` without a direct dep on `B`. If `B` were listed in `deps` instead, only `A`'s own sources could include from `B` — all of `A`'s consumers would fail `gn check` unless they added a direct dep on `B` themselves. Practical consequence: hundreds of browser-test targets dep on `//chrome/test:test_support` (or `test_support_ui`) and freely include any `chrome/test/base/*.h` header. During Phase 2, if `launchservices_utils_mac` were moved to `deps` (not `public_deps`) in `test_support`, every consumer that includes `launchservices_utils_mac.h` would break. Adding it to `public_deps` preserves the status quo and keeps each extraction CL a pure BUILD-graph-only change with no consumer disruption. This is also why `test_support_ui` needed explicit `public_deps` additions for every extracted mac/win/CrOS target whose headers it includes — `test_support_ui` does not dep on `test_support`, so extracted targets do not propagate to it transitively. ### Phase 3b — progress Phase 3b is underway as a series of BUILD.gn-only CLs, each removing one `//chrome/test/base:*` entry from `//chrome/test:test_support`'s `public_deps` by adding direct deps to all consumers. **CL series (3b-9–3b-15 ✅ landed 2026-09-16; 3b-16–3b-22 🔧 fixups applied, re-uploading):** | CL | Target removed from `test_support` `public_deps` | Consumers | Status | |----|--------------------------------------------------|-----------|--------| | [3b-9](https://chromium-review.googlesource.com/8341312) | `profile_lifecycle_test_utils` | 9 | ✅ landed | | [3b-10](https://chromium-review.googlesource.com/8341313) | `browser_test_waiters` | 18 | ✅ landed | | [3b-11](https://chromium-review.googlesource.com/8341314) | `chrome_test_scoped_utils` | 7 | ✅ landed | | [3b-12](https://chromium-review.googlesource.com/8341315) | `devtools_coverage` | 4 | ✅ landed | | [3b-13](https://chromium-review.googlesource.com/8350246) | `scoped_metrics_service_for_synthetic_trials` | 2 | ✅ landed | | [3b-14](https://chromium-review.googlesource.com/8350247) | `web_feature_histogram_tester`, `web_ui_test_data_source`, `scoped_channel_override` | 3 | ✅ landed | | [3b-15](https://chromium-review.googlesource.com/8341316) | `chrome_render_view_test`, `chrome_test_path_utils`, `platform_browser_test`, `android_ui_test_utils`, misc small | ~15 | ✅ landed | | [3b-16](https://chromium-review.googlesource.com/8350249) | `chrome_render_view_host_test_harness` | 209 | 🔧 fixup: `chrome_render_view_host_test_harness` → `renderer_host:unit_tests` (mac) | | [3b-17](https://chromium-review.googlesource.com/8350250) | `testing_browser_process_impl` | 344 | 🔧 fixup: `testing_browser_process` → `media:unittests` win block | | [3b-18](https://chromium-review.googlesource.com/8350251) | `testing_profile_impl` (partial — cycle blocker) | 1 | 🔧 rebased on 3b-17 fixup | | [3b-19](https://chromium-review.googlesource.com/8350252) | `browser_with_test_window_test`, `test_browser_window` | 60 | 🔧 rebased on 3b-18 | | [3b-20](https://chromium-review.googlesource.com/8350253) | `drag_and_drop_test_utils`, `test_theme_provider` | 3 | 🔧 fixup: `drag_and_drop_test_utils` → `use_aura` guard in `interactive_ui_tests` + `glic:browser_tests` (mac) | | [3b-21](https://chromium-review.googlesource.com/8350254) | chromeos-only quartet | 13 | 🔧 fixup: `cros_extension_js_browser_test` → 8 accessibility js2gtest targets (cros) | | [3b-22](https://chromium-review.googlesource.com/8350255) | android/mac/win platform-specific targets | 4 | 🔧 rebased on 3b-21 fixup | After 3b-22 lands, `//chrome/test:test_support`'s `public_deps` will have **zero** `//chrome/test/base:*` entries (for all locally testable platforms: linux/cros/android/android-desktop). Mac/win entries (`test_utils_mac`, `process_helpers_win`, `scoped_channel_override`) removed; pending CQ validation. **Follow-up for next round of CLs (3b-23+):** `testing_profile_impl` remains in `test_support`'s `public_deps` across the entire 3b-17→3b-22 stack — the cycle `testing_profile_impl → extensions:test_support → identity_test_support` blocks removal. This has a cascading consequence: `testing_profile_impl` calls `TestingBrowserProcess` methods from `testing_browser_process_impl`, so removing `testing_browser_process_impl` from `public_deps` (3b-17) caused linker failures. Fix: `testing_browser_process_impl` added to `test_support`'s `deps` as a temp dep with a TODO pointing to this blocker. The three follow-up steps for the next round: 1. **3b-23 — Break the cycle:** Remove auto-registration of `TestExtensionSystem::Build` from `testing_profile.cc`; add `GetExtensionSystemTestingFactories()` helper to `extensions:test_support` following the `GetIdentityTestEnvironmentFactories()` pattern. Callers (e.g. `extension_service_test_base.cc`) call `AddTestingFactories(GetExtensionSystemTestingFactories())` explicitly. Local branch exists, not yet uploaded. 2. **Remove `testing_profile_impl` from `test_support`'s `public_deps`** — unblocked once 3b-23 lands. 3. **Remove temp `testing_browser_process_impl` dep from `test_support`'s `deps`** in `chrome/test/BUILD.gn` — unblocked once step 2 is done (TODO comment in code gates on this). **Lessons learned in Phase 3b:** 1. **`static_library` vs `source_set` dep propagation:** `//chrome/test:test_support` is a `static_library`. When an entry is moved from `public_deps` to `deps`, it no longer propagates to consumers' link chains. `gn gen --check` passes (header visibility is preserved) but the build fails with linker errors. Always validate with a full build after demoting a dep, not just `gn check`. 2. **T / T_impl header-only split:** When a target pair like `testing_browser_process` / `testing_browser_process_impl` is involved — where `testing_browser_process` is **headers-only** (zero `.cc` files) and all implementations live in `testing_browser_process_impl` — migrating consumers to dep on `testing_browser_process` alone is insufficient for linking. The impl's object files must also be in the final binary's dep chain. Fix: add `testing_browser_process_impl` to the aggregator/executable targets (`test("unit_tests")`, `platform_browser_tests`, etc.) rather than to each of the 344 individual consumers. 3. **`assert(use_aura)` on mac:** Adding `//ui/aura:test_support` to a target inside `if (!is_chromeos_device)` (but without a `use_aura` guard) causes GN gen to fail on mac, because `//ui/aura:test_support` has `//ui/wm` as an unconditional dep and `//ui/wm/BUILD.gn` has `assert(use_aura)`. Guard any direct dep on `//ui/aura` or `//ui/aura:test_support` with `if (use_aura)`. 4. **`gn --fix` spurious `public_deps`:** When `gn --fix` adds a dep inside an existing `deps +=` block, it sometimes _also_ emits a standalone `public_deps = [...]` block at the end of the target — creating a duplicate. Always review `gn --fix` output for spurious `public_deps` blocks. 5. **Mac-only consumers missed by linux/cros GN check:** A consumer target with a `.mm` source (e.g. `renderer_host:unit_tests`) is mac-only. If no mac bot runs on a given CL, the missing dep goes undetected on linux/cros. When removing a target from `test_support`'s `public_deps`, always audit for mac-only consumers even when GN check passes locally. Fix: add the direct dep (e.g. `//chrome/test/base:chrome_render_view_host_test_harness`) to the target's `deps`. 6. **Win-only consumers hidden behind platform blocks:** Windows-only sources in a `if (is_win)` block add deps that don't show in linux/cros GN check. When a 3b CL changes `profiles:test_support` to add a private dep (e.g. `testing_browser_process`), consumers that previously reached it transitively through `c/t:test_support` are broken on Windows. Fix: add the missing dep (e.g. `//chrome/test/base:testing_browser_process`) to the `if (is_win) { deps += [...] }` block of the consumer. 7. **Aura-only targets require `use_aura` guards even in `!is_android` blocks:** `drag_and_drop_test_utils` depends on `//ui/aura`, making it an aura-only target. Adding it to `deps` inside `if (!is_android)` is not sufficient — mac is `!is_android` but is not aura. The correct guard is `if (use_aura)`. The same caution applies to any target whose transitive deps include `//ui/aura`, `//ui/wm`, or `//ui/aura:test_support`. 8. **js2gtest `test_type = "extension"` consumers need `cros_extension_js_browser_test` direct dep:** Generated C++ from js2gtest targets with `test_type = "extension"` inherits from `ExtensionJSBrowserTest`. These targets must have a direct `deps` entry on `//chrome/test/base:cros_extension_js_browser_test`. Previously this was provided transitively via `c/t:test_support`. After 3b-21 removed it, all 8 accessibility js2gtest targets in `chrome/browser/resources/chromeos/accessibility/*/BUILD.gn` needed explicit deps added. ### Phase 3b — consumer migration description The `public_deps` entries in the monolith are transitional. The long-term goal is that every target depending on an extracted module does so directly, without going through the monolith. That cleanup is Phase 3b. Migration process for each extracted target (e.g. `:launchservices_utils_mac`): 1. Run `gn refs out/mac //chrome/test/base:launchservices_utils_mac` to enumerate all GN targets that (transitively) consume it. 2. For each external consumer, check whether its sources `#include "chrome/test/base/launchservices_utils_mac.h"`. Consumers that do should add `//chrome/test/base:launchservices_utils_mac` as a direct `deps` entry in their `BUILD.gn`. 3. Once all consumers have direct deps, demote the monolith's entry from `public_deps` → `deps`. 4. When the monolith has no remaining `public_deps` entries (all converted to direct consumer deps), it becomes a pure thin aggregator — Phase 3a — and can eventually be deleted. > [!note] This directly answers reviewer questions of the form: *"Do you plan to make callers that include `launchservices_utils_mac.h` depend on it directly rather than transitively?"* > Yes — that is Phase 3b. The `public_deps` entries added during Phase 2 are a deliberate, time-bounded transitional pattern, not the intended end-state. ### Why deferred to Phase 3 The consumer list for a popular header can span dozens or hundreds of browser-test targets. Migrating all of them during Phase 2 would inflate each extraction CL with unrelated `BUILD.gn` edits and complicate review. Phase 3b is cleanly separable — extracting targets into `public_deps` is safe and does not block further modularization. Batching the migration into a dedicated phase (after all sources are extracted) keeps Phase 2 CLs small, focused, and reviewable. --- ## References - [Project Bedrock design doc](https://chromium.googlesource.com/chromium/src/+/main/docs/modularization.md) - `chrome/test/interaction/BUILD.gn` — first CL in this effort; reference for BUILD.gn pattern - `chrome/test/base/BUILD.gn` — active extraction target - `chrome/test/BUILD.gn` — monolith, being reduced - Bug tracker: [crbug.com/353332589](https://crbug.com/353332589)