Angular on Bun (bun:test)
Angular has no bun test integration of its own. Two things are missing, and both are fatal on their own:
- No DOM. Bun ships none, and everything from
platformBrowserTesting()onwards readsdocument. - No template resolution.
@Component({ templateUrl: './x.html' })is not an import — nothing in the module graph points at the HTML file — so Angular's JIT compiler refuses to build the component ("Component X is not resolved"). Under Vitest,@analogjs/vite-plugin-angularinlines it during transform. Bun has no such transform.
vitest-auto-spy/bun-angular closes both, plus the wiring around them, from a single preload.
Setup
# bunfig.toml
[test]
preload = ["vitest-auto-spy/bun-angular"]bun add -d @happy-dom/global-registrator # or: bun add -d jsdomThat is the whole configuration. On load the entry:
- installs a DOM —
@happy-dom/global-registratorif present, otherwisejsdom, and nothing at all if a DOM is already there; - registers a
Bun.pluginonLoadhook that inlinestemplateUrl/styleUrl/styleUrlsinto the component source; - initialises a zoneless
TestBedenvironment and resets the testing module after each test; - registers the Bun mock adapter, so every spy helper is Bun's.
It has to be a preload
A Bun.plugin hook only sees modules loaded after it is registered. Importing this entry from inside a spec is too late for the component under test — its template will not be inlined. Importing it from a spec as well is fine: the module is cached and every step is guarded.
Writing a spec
From here a spec reads exactly like its Vitest counterpart.
// greeting.test.ts
import { TestBed } from '@angular/core/testing';
import { describe, expect, it } from 'bun:test';
import { GreetingComponent } from './greeting.component'; // declared with templateUrl
import { GreetingService } from './greeting.service';
import { injectSpy, provideAutoSpy, stable } from 'vitest-auto-spy/bun-angular';
describe('GreetingComponent', () => {
it('renders the name the service returns', async () => {
TestBed.configureTestingModule({ providers: [provideAutoSpy(GreetingService)] });
injectSpy(GreetingService).currentName.mockReturnValue('external user');
const fixture = TestBed.createComponent(GreetingComponent);
await stable(fixture);
expect(fixture.nativeElement.textContent).toContain('Hello, external user!');
});
});bun test # add --isolate for a fresh global per fileWhat you get
| Helper | Works on Bun | Notes |
|---|---|---|
provideAutoSpy / injectSpy | ✅ | identical to the Vitest entry, lazy spies by default |
renderShallow | ✅ | real ComponentFixture, child subtree dropped |
createWithAutoSpies | ✅ | builds a class through Angular DI with every dep spied |
stable / flushEffects | ✅ | zoneless waiting |
the whole core (createSpyFromClass, …) | ✅ | re-exported from this entry |
registerSignalMatchers | ❌ | needs the runner's expect.extend — Vitest only |
TestBed diagnostics (instrumentTestBed) | ❌ | needs suite-level runner hooks — Vitest only |
Stylesheets
A test runner has no CSS pre-processor, and no spec asserts on styles. So .css is inlined verbatim and everything else (.scss, .less, .styl) becomes an empty stylesheet — the component still compiles and renders. Override it if you genuinely need the text:
inlineAngularResources(source, path, { inlineStyleExtensions: ['.css', '.scss'] });Building your own preload
Every piece is exported, so a project with its own preload can compose them instead of taking the defaults:
// bun-preload.ts
import { createJsdomRegistrar, inlineAngularResources, registerDomGlobals } from 'vitest-auto-spy/bun-angular';
await registerDomGlobals({
registrars: [createJsdomRegistrar({ load: () => import('jsdom'), target: globalThis, url: 'https://app.test/' })],
});registerDomGlobals returns the name of the registrar that installed the DOM, or undefined when one was already present, and throws with every attempt listed when none worked.
Limits worth knowing
- The rewrite is textual, not a parse. It skips comments and string literals — a
templateUrlwritten in prose is left alone — but it does not track${…}interpolation or regex literals. - Line numbers are preserved. Every inlined value is a single-line literal, so a failing spec's stack trace still points at the component's own line.
node_modulesis skipped. Published Angular libraries are already compiled.- This entry is ESM-only. It awaits its DOM registrar at the top level, which has no CommonJS form. Bun runs ESM natively, so nothing is lost.