>[!warning]- Info
> Author:
[email protected]
> Publish date: 2026/07/02
> Last update: 2026/08/22
> Status: WIP
> Revision: 35
> 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** (2 CLs) | ⬜ ready to push | CL1 (`chromod/chrome-test-base-2d-1`): inline process_inspector_win→process_lineage_win, merge mac utils→test_utils_mac, inline js_test_api→javascript_browser_test, inline web_ui_test_handler→cros_web_ui_browser_test. CL2 (`chromod/chrome-test-base-2d-2`): inline in_process_browser_test_mixin→mixin_based, merge test_browser_window_aura→test_browser_window, inline web_view_focus_helper+window_contents_as_string_win→interactive_test_utils. Net: ~62→~51 targets. Both presubmit-clean. |
| Phase 3 — remaining dirs (`android/` leftovers…) | ⬜ | After `base/` complete |
| Phase 3a — Monolith → thin aggregator | ⬜ | After ALL sources extracted |
| Phase 3b — Consumer migration + monolith deletion | ⬜ | After 3a; requires coordination |
> [!tip] Phase 2a thin targets
> `:in_process_browser_test` and `:ui_test_utils` are currently header-only. Their `.cc` implementations remain in the monolith until the pre-existing cycle `//chrome/browser/ui:test_support → //chrome/test:test_support_ui` is broken. `:test_launcher_utils` is now fully extracted (CL10).
---
## 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_window_waiters` | `browser_{closed,created}_waiter.{cc,h}` | Full; see improvement note below |
| `:browser_feature_test_waiters` | `autocomplete_change_observer.{cc,h}`, `find_result_waiter.{cc,h}`, `search_test_utils.{cc,h}` | Full (CL10) |
| `: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_data_source` | `web_ui_test_data_source.{cc,h}` | Full (CL11d) |
| `: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 |
| `:test_chrome_web_ui_controller_factory` | `test_chrome_web_ui_controller_factory.{cc,h}` | Full (CL16b); non-android |
| `: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 by 2D-1 | Full (CL27 + 2D-1); chromeos only; from `test_support_ui`; needed explicit `chrome_content_browser_client` private dep |
| `: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 |
| `:mojo_web_ui_browser_test` | `ash/mojo_web_ui_browser_test.{cc,h}` | Full (CL28); chromeos only; from `test_support_ui`; needed explicit `chrome_content_browser_client` private 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-22, CL26–31 landed, CL32–37 + Phase 2D ready to push)
| 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 | **`:chrome_render_view_test` (CL35) + TBP split** |
**CL26–31 landed. CL32–37 ready to push.** CL37 extracted `:chrome_ash_test_base`, `:browser_tests_runner`, `crosier:ash_integration_test_support`, and `:browser_tests` (7 base/ browsertests). CL35 extracts `:chrome_render_view_test`; CL36 trims 111 stale dep lines from `test_support` deps/public_deps.
### Phase 2D — Granularity Review
BUILD.gn-only consolidation pass: 9 single-consumer targets inlined into their only
consumer. Net: ~62 → ~51 targets (11 fewer). Two CLs, both presubmit-clean, ready to push.
**CL 2D-1** (`chromod/chrome-test-base-2d-1`): 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`
**CL 2D-2** (`chromod/chrome-test-base-2d-2`): 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)
Note: `always_on_top_window_killer_win` kept separate — 2 consumers. Win chain could not be fully collapsed (original plan assumed 4→1; actual 4→2).
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
---
### Phase 2c — cluster map (audited 2026-07-14)
Dep analysis done by reading all remaining `.h`/`.cc` files and cross-referencing with `gn gen --ide=json` project.json for precise header→target resolution.
#### Future improvement: `:browser_window_waiters` → separate `:test_browser_window` target
`test_browser_window.{cc,h}` and `dialog_test_browser_window.{cc,h}` also include `browser_collection_observer.h` and thus dep on `//chrome/browser/ui/browser_window`, same as the two waiters. However, `test_browser_window.h` carries **much heavier** additional deps:
- `chrome/browser/ui/browser.h` — full `Browser` class
- `chrome/browser/ui/autofill/test/test_autofill_bubble_handler.h`
- `chrome/browser/ui/dialogs/browser_dialogs.h`
- `chrome/browser/ui/location_bar/location_bar.h`
- `chrome/browser/ui/translate/partial_translate_bubble_model.h`
And `dialog_test_browser_window.h` includes `test_browser_window.h` directly. Both are non-android only.
**Decision:** keep `:browser_window_waiters` lightweight (all-platform, waiter-only). Extract `test_browser_window` + `dialog_test_browser_window` as a **separate** `:test_browser_window` target (non-android, heavier Browser+UI dep footprint). Do not merge into `:browser_window_waiters` — that would force the heavy Browser class on consumers that only need the waiters.
#### Forced cluster: `:test_suites`
Files: `chrome_test_suite.{cc,h}` + `chrome_unit_test_suite.{cc,h}`
Reason: `chrome_unit_test_suite.h` inherits from `ChromeTestSuite` and includes `chrome_test_suite.h` directly — tightly coupled.
Heavy deps: `//chrome/app:test_support`, `//chrome/browser:browser_process`, `//chrome/browser/ui/webui:configs`, `//chrome/browser/ui:ui`, `//chrome/browser/profiles:profile_manager`, `//chrome/browser/lifetime`, `//chrome/common:constants`, `//chrome/test/base:testing_browser_process`, and more. Biggest lift remaining.
#### Separate targets despite similar names: `chrome_render_view_*`
- `chrome_render_view_host_test_harness` — browser-side, wraps `TestingProfile`; deps: `:testing_profile` + `//content/test:test_support`
- `chrome_render_view_test` — renderer-side, no `Profile`; deps: `//content/test:test_support`, `//services/service_manager/public/cpp:cpp`, `//third_party/blink/public/common:headers`
Different process boundary, different dep profiles → extract as separate targets.
#### Clean standalones (deps fully audited)
✅ CL10: `autocomplete_change_observer`, `find_result_waiter`, `search_test_utils` → `:browser_feature_test_waiters`; `scoped_browser_locale` → `:chrome_test_scoped_utils`.
✅ CL11b–11c: `tracing` → `:tracing`; `web_feature_histogram_tester` → `:web_feature_histogram_tester`.
✅ CL12a–13a: `platform_browser_test` (thin header), `scoped_metrics_service_for_synthetic_trials`, `chrome_render_view_host_test_harness`, `devtools_coverage`, `chrome_test_utils` (thin header; `.cc` deferred — TBP dep cycle).
#### ~~Complete existing thin target: `:test_launcher_utils`~~ ✅ Done in CL10
Added `test_launcher_utils.cc` — deps: `//chrome/common`, `//components/os_crypt/common:switches`, `//components/password_manager/core/browser:password_switches`, `//content/public/common`, `//ui/display`; aura-only: `//ui/wm`.
#### ~~Blocked: `chrome_test_utils`~~ ✅ Done in CL13a (thin header)
`chrome_test_utils.h` extracted as thin header-only target. The `.cc` stays in the monolith until the TBP dep-graph cycle is resolved via Pattern A split.
#### Suggested extraction order
1. ✅ `:browser_window_waiters` — [CL9](https://chromium-review.googlesource.com/8094907)
2. ✅ `:browser_feature_test_waiters` + `:chrome_test_scoped_utils` + `:test_launcher_utils` (complete) — [CL10](https://chromium-review.googlesource.com/8100982)
3. ✅ `:tracing` — [CL11b](https://chromium-review.googlesource.com/8138555); extracted as standalone (not merged into `:chrome_test_scoped_utils`)
4. ✅ `:web_feature_histogram_tester` — [CL11c](https://chromium-review.googlesource.com/8138556)
5. ✅ `:platform_browser_test` thin header — CL12a; unblocked `chrome_test_utils`
6. ✅ `:scoped_metrics_service_for_synthetic_trials` — CL12b
7. ✅ `:chrome_render_view_host_test_harness` — CL12c
8. ✅ `:devtools_coverage` — CL12d
9. ✅ `:chrome_test_utils` thin header — CL13a; `.cc` deferred (TBP dep cycle)
10. ✅ `:chrome_test_path_utils` — [CL18b](https://chromium-review.googlesource.com/8253803); breaks dep-graph cycle; unlocks core cluster
11. 🔄 `:chrome_test_suite` thin + `:in_process_browser_test` complete — [CL19](https://chromium-review.googlesource.com/8253804) (CQ)
12. 🔄 `:ui_test_utils` complete — [CL20](https://chromium-review.googlesource.com/8253805) (CQ)
13. 🔄 `:interactive_test_utils` — [CL21](https://chromium-review.googlesource.com/8253806)/[CL22](https://chromium-review.googlesource.com/8253807) (CQ)
14. 🔄 `:mixin_based_in_process_browser_test` — [CL23](https://chromium-review.googlesource.com/8253808) (CQ)
15. 🔄 `:fake_gaia_mixin` — [CL24](https://chromium-review.googlesource.com/8253809) (CQ)
16. 🔄 `:web_view_focus_helper` — [CL25](https://chromium-review.googlesource.com/8253810) (CQ)
17. 🔄 `:javascript_browser_test` + `:scoped_test_system_nss_key_slot_mixin` — [CL26](https://chromium-review.googlesource.com/8264099) (in CQ)
18. 🔄 `:web_ui_browser_test` + `:extension_js_browser_test` — [CL27](https://chromium-review.googlesource.com/8264100) (in CQ)
19. 🔄 `:mojo_web_ui_browser_test` — [CL28](https://chromium-review.googlesource.com/8264101) (in CQ)
20. 🔄 `:menu_model_test` — [CL29](https://chromium-review.googlesource.com/8271139) (in CQ, needs rebase)
21. 🔄 Android cluster — [CL30](https://chromium-review.googlesource.com/8271140) (in CQ)
22. 🔄 Mac additions — [CL31](https://chromium-review.googlesource.com/8271141) (in CQ, PS3 active)
23. ⬜ `:chrome_render_view_test` — renderer-side (CL35, ready to push)
24. ⬜ `:browser_with_test_window_test` — heavy Browser + ash + views deps (CL34, ready to push)
25. ⬜ `:chrome_test_suite` complete (add `.cc`) — CL32, ready to push
26. ⬜ `:chrome_unit_test_suite` — CL33, ready to push
27. ⬜ Trim stale deps from `test_support` — CL36, ready to push (111 lines removed; 4 configs gn-check clean)
28. ⬜ `:chrome_ash_test_base` + `:browser_tests_runner` (whole target) + `crosier:ash_integration_test_support` + `:browser_tests` (7 base/ browsertests) — CL37, ready to push
### 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.
### Phase 2c — remaining `base/` sources to audit and extract
| Files | Likely complexity | Notes |
|-------|------------------|-------|
| `ash/javascript_browser_test.{cc,h}` | Low–medium | CrOS (test_support_ui); includes `in_process_browser_test.h` — now extracted; **CL26** |
| `ash/scoped_test_system_nss_key_slot_mixin.{cc,h}` | Low–medium | CrOS (test_support); includes `mixin_based_in_process_browser_test.h` — now extracted; **CL26** |
| `ash/web_ui_browser_test.{cc,h}`, `ash/extension_js_browser_test.{cc,h}` | Medium | CrOS; depend on `javascript_browser_test.h`; **CL27** |
| `ash/mojo_web_ui_browser_test.{cc,h}` | Medium | CrOS; depends on `web_ui_browser_test.h`; **CL28** |
| `chrome_render_view_test.{cc,h}` | Medium | Renderer-side; no Profile dep; candidate after CrOS cluster |
| `chrome_test_utils.cc` | Low–medium | Deferred until TBP Pattern A split |
| `chrome_test_suite.cc`, `chrome_unit_test_suite.{cc,h}` | High | Test suite entry-points; many deps; separate planning session |
| Android variants (`interactive_test_utils.{cc,h}`, `mixin_based_in_process_browser_test.{cc,h}` in `test_support_ui_android`) | Medium | Android monolith; separate effort |
### 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 — consumer migration
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)