|
| 1 | +--- |
| 2 | +name: foundation-test-migration |
| 3 | +description: Migrating unit tests to foundation unit tests using TestUniverse and devtools_foundation_module. Use when moving tests away from DOM-heavy helpers like describeWithEnvironment or describeWithMockConnection. |
| 4 | +--- |
| 5 | + |
| 6 | +# Foundation Test Migration |
| 7 | + |
| 8 | +This skill provides guidance on migrating DevTools unit tests to the "foundation" pattern, which is lighter, avoids global singletons, and is compatible with both Node and Browser runtimes (Isomorphic). |
| 9 | + |
| 10 | +## Core Concepts |
| 11 | + |
| 12 | +### devtools_foundation_module (BUILD.gn) |
| 13 | +Use this template for modules that should be platform-agnostic. |
| 14 | +- **Enforcement**: It type-checks the code against both Browser and Node APIs. |
| 15 | +- **Constraint**: Avoid direct DOM access (like `FileReader` or layout metrics) or heavy DevTools dependencies. Use `Universe` to access services. |
| 16 | + |
| 17 | +### TestUniverse |
| 18 | +`TestUniverse` is the preferred way to setup a DevTools-like environment for tests without global singletons. |
| 19 | +- **Lazy**: Dependencies (targetManager, settings, workspace, etc.) are only created when accessed via getters. |
| 20 | +- **Scoped**: Does not install instances as globals (avoids `Common.Settings.Settings.instance()`). |
| 21 | +- **Explicit**: Uses `DevToolsContext` to manage dependencies. |
| 22 | + |
| 23 | +## Migration Guide |
| 24 | + |
| 25 | +### 1. Replace Heavy Helpers |
| 26 | +Avoid `describeWithEnvironment` or `describeWithMockConnection`. |
| 27 | + |
| 28 | +Instead, use standard `describe` and initialize environment hooks at the top-level `describe` block. **Crucial:** Without `setupRuntimeHooks`, tests creating SDK models will crash due to uninitialized experiments (e.g. `capture-node-creation-stacks`). |
| 29 | + |
| 30 | +```ts |
| 31 | +import {setupLocaleHooks} from '../../testing/LocaleHelpers.js'; |
| 32 | +import {setupSettingsHooks} from '../../testing/SettingsHelpers.js'; |
| 33 | +import {setupRuntimeHooks} from '../../testing/RuntimeHelpers.js'; |
| 34 | +import {TestUniverse} from '../../testing/TestUniverse.js'; |
| 35 | + |
| 36 | +describe('MyComponent', () => { |
| 37 | + setupLocaleHooks(); |
| 38 | + setupSettingsHooks(); |
| 39 | + setupRuntimeHooks(); |
| 40 | + |
| 41 | + let universe: TestUniverse; |
| 42 | + |
| 43 | + beforeEach(() => { |
| 44 | + universe = new TestUniverse(); |
| 45 | + }); |
| 46 | +}); |
| 47 | +``` |
| 48 | + |
| 49 | +### 2. Access Dependencies via Universe |
| 50 | +Instead of using `SDK.TargetManager.TargetManager.instance()`, use `universe.targetManager`. Use `universe.createTarget()` instead of the global `createTarget`. |
| 51 | + |
| 52 | +```ts |
| 53 | +// OLD |
| 54 | +const target = createTarget(); |
| 55 | +const targetManager = SDK.TargetManager.TargetManager.instance(); |
| 56 | + |
| 57 | +// NEW |
| 58 | +const target = universe.createTarget({url: urlString`http://example.com/`}); |
| 59 | +const targetManager = universe.targetManager; |
| 60 | +``` |
| 61 | + |
| 62 | +#### Mocking CDP Traffic |
| 63 | +If the test used `describeWithMockConnection` and global `setMockConnectionResponseHandler` to stub CDP traffic, you can migrate this by passing a `MockCDPConnection` to `universe.createTarget()`. This allows stubbing out CDP traffic scoped to a specific target tree rather than globally. |
| 64 | + |
| 65 | +```ts |
| 66 | +import {MockCDPConnection} from '../../testing/MockCDPConnection.js'; |
| 67 | + |
| 68 | +const cdpConnection = new MockCDPConnection([ |
| 69 | + { |
| 70 | + method: 'Network.getResponseBody', |
| 71 | + response: () => ({body: 'mocked body', base64Encoded: false}), |
| 72 | + } |
| 73 | +]); |
| 74 | + |
| 75 | +const target = universe.createTarget({connection: cdpConnection}); |
| 76 | +``` |
| 77 | + |
| 78 | +> [!TIP] |
| 79 | +> **Legacy Target URLs**: `EnvironmentHelpers.createTarget()` defaults to `http://example.com/`. `TestUniverse.createTarget()` defaults to `about:blank`. If your test asserts against specific URLs, remember to pass the URL explicitly. |
| 80 | +
|
| 81 | +### 3. Dealing with Legacy Singletons & Helpers |
| 82 | + |
| 83 | +For large integration tests, you may encounter code that strictly calls `SomeModule.instance()` or uses complex legacy helpers (like `createWorkspaceProject`). |
| 84 | + |
| 85 | +**Do not use `setUpEnvironment()`** as it will create disconnected singletons. Instead, wire the singletons to your `TestUniverse` and stub the globals to bridge legacy helpers: |
| 86 | + |
| 87 | +```ts |
| 88 | +beforeEach(async () => { |
| 89 | + universe = new TestUniverse(); |
| 90 | + const {targetManager, workspace, settings} = universe; |
| 91 | + |
| 92 | + // 1. Stub globals so legacy helpers use TestUniverse components |
| 93 | + sinon.stub(Workspace.Workspace.WorkspaceImpl, 'instance').returns(workspace); |
| 94 | + sinon.stub(SDK.TargetManager.TargetManager, 'instance').returns(targetManager); |
| 95 | + sinon.stub(Common.Settings.Settings, 'instance').returns(settings); |
| 96 | + |
| 97 | + // 2. Initialize interdependent singletons in the correct order |
| 98 | + SDK.NetworkManager.MultitargetNetworkManager.instance({forceNew: true, targetManager}); |
| 99 | + |
| 100 | + // 3. Now safe to use legacy helpers that rely on the above instances |
| 101 | + await createWorkspaceProject(urlString`file:///path/to/overrides`, [...]); |
| 102 | +}); |
| 103 | +``` |
| 104 | + |
| 105 | +## Pitfalls & Troubleshooting |
| 106 | + |
| 107 | +### Strict Equality in Protocol Responses (`assert.deepEqual`) |
| 108 | +Protocol requests/responses dynamically generated by Chrome (like Network conditions) can vary slightly (e.g., adding `connectionType`, `urlPattern`). |
| 109 | +- **Problem**: `assert.deepEqual(rules, [{...}])` will flake if unrequested fields are present. |
| 110 | +- **Solution**: Use `sinon.spy()` for handlers and assert with `sinon.assert.calledOnceWithMatch`: |
| 111 | + |
| 112 | +```ts |
| 113 | +const emulateSpy = sinon.spy(); |
| 114 | +connection.setHandler('Network.emulateNetworkConditionsByRule', request => { |
| 115 | + emulateSpy(request); |
| 116 | + return {result: {ruleIds: []}}; |
| 117 | +}); |
| 118 | + |
| 119 | +// Matches only the fields you care about, ignoring extra protocol fields |
| 120 | +sinon.assert.calledOnceWithMatch(emulateSpy, { |
| 121 | + offline: false, |
| 122 | + matchedNetworkConditions: [sinon.match({ downloadThroughput: 1000 })], |
| 123 | +}); |
| 124 | +``` |
| 125 | + |
| 126 | +### DOM Globals (`ReferenceError: FileReader is not defined`) |
| 127 | +Foundation tests run in Node.js where `window`, `FileReader`, and certain DOM string encodings don't exist. |
| 128 | +- **Fix**: Use isomorphic equivalents (e.g. `btoa()`, `Uint8Array`). |
| 129 | +- **Fix**: Abstract the APIs that are different between Node.js and Browser via `front_end/core/platform/api/HostRuntime.ts`. |
| 130 | + |
| 131 | +### Initialization Order Lockups |
| 132 | +If a test times out (5000ms exceeded), it is usually an unhandled promise caused by a missing singleton. Double-check the constructor of the failing manager to see which `instance()` it listens to, and ensure that dependent singleton was created *first*. |
| 133 | + |
| 134 | +## BUILD.gn Changes |
| 135 | + |
| 136 | +When a module and its tests are ready, update `BUILD.gn`: |
| 137 | + |
| 138 | +1. Change `devtools_module` to `devtools_foundation_module` for both the module and its unittests. |
| 139 | +2. Ensure the tests are grouped under a `foundation_unittests` target in the parent `BUILD.gn`. |
| 140 | + |
| 141 | +```gn |
| 142 | +# front_end/my_module/BUILD.gn |
| 143 | +
|
| 144 | +devtools_foundation_module("my_module") { |
| 145 | + sources = [ "MyModule.ts" ] |
| 146 | + deps = [ "../../core/common:bundle" ] |
| 147 | +} |
| 148 | +
|
| 149 | +devtools_foundation_module("unittests") { |
| 150 | + testonly = true |
| 151 | + sources = [ "MyModule.test.ts" ] |
| 152 | + deps = [ |
| 153 | + ":my_module", |
| 154 | + "../../testing", |
| 155 | + ] |
| 156 | +} |
| 157 | +``` |
| 158 | + |
| 159 | +## Verification |
| 160 | + |
| 161 | +Foundation tests must pass in both Node.js and Browser runtimes. |
| 162 | + |
| 163 | +### Run in Node.js |
| 164 | +```bash |
| 165 | +npm test -- front_end/core/sdk/NetworkManager.test.ts --node-unit-tests |
| 166 | +``` |
| 167 | + |
| 168 | +### Run in Browser |
| 169 | +```bash |
| 170 | +npm test -- front_end/core/sdk/NetworkManager.test.ts |
| 171 | +``` |
0 commit comments