/* eslint-disable @typescript-eslint/no-explicit-any */ import { plugin } from 'bun'; import { readFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import * as bt from 'bun:test'; // shortcut: imports Analog's internal jitTransform by path; replace when Angular or Analog exports a JIT transform. import { jitTransform } from '../node_modules/@analogjs/vite-plugin-angular/src/lib/compiler/jit-transform.js'; import { vi } from './testing/vitest-compat'; // Inline templateUrl/styleUrl(s) before Bun transpiles: JIT has no resource loader. plugin({ name: 'angular-jit', setup(build) { build.onLoad({ filter: /src\/app\/(?!.*\.spec\.ts$).*\.ts$/ }, ({ path }) => { let src = readFileSync(path, 'utf8'); if (!/@(Component|Directive|Injectable|Pipe|NgModule)\(/.test(src)) { return { contents: src, loader: 'ts' }; } const read = (p: string) => JSON.stringify(readFileSync(resolve(dirname(path), p), 'utf8')); src = src.replace(/templateUrl:\s*'([^']+)'/, (_, p) => `template: ${read(p)}`); // Styles are not compiled; they do not matter to the tests. src = src.replace(/styleUrl:\s*'([^']+)'/, 'styles: []'); src = src.replace(/styleUrls:\s*\[[^\]]*\]/, 'styles: []'); // Signal input()/output()/viewChild() become decorators and @Component becomes ɵcompileComponent. return { contents: jitTransform(src, path).code, loader: 'ts' }; }); }, }); const { JSDOM } = await import('jsdom'); const dom = new JSDOM('', { url: 'http://localhost:3000/', pretendToBeVisual: true, }); const win = dom.window as any; const keep = new Set([ 'console', 'globalThis', 'undefined', 'Infinity', 'NaN', 'Object', 'Function', 'Array', 'Error', 'Promise', 'Symbol', 'Map', 'Set', 'JSON', 'Math', 'Date', 'process', 'window', 'self', ]); const force = new Set([ 'document', 'navigator', 'location', 'localStorage', 'sessionStorage', 'history', 'getComputedStyle', ]); const domNames = /^(HTML|SVG|DOM|Mutation|Node|Element|Event|Text|Comment|Document|Range|Selection|CSS|Shadow|Custom|Keyboard|Mouse|Focus|Input|UI|Wheel|Pointer|Touch|Animation|File|Blob|FormData|Window|Storage|Headers|Abort)/; for (const k of Object.getOwnPropertyNames(win)) { if (keep.has(k) || (k in globalThis && !force.has(k) && !domNames.test(k))) continue; // Live accessors: spying on window.x also affects bare x (jsdom window !== globalThis here). Object.defineProperty(globalThis, k, { configurable: true, get: () => { const v = win[k]; return typeof v === 'function' && /^[a-z]/.test(k) ? v.bind(win) : v; }, set: (v) => { win[k] = v; }, }); } for (const k of ['window', 'self']) { Object.defineProperty(globalThis, k, { configurable: true, writable: true, value: win }); } Object.assign(globalThis, { vi, describe: bt.describe, it: bt.it, test: bt.test, expect: bt.expect, beforeEach: bt.beforeEach, afterEach: bt.afterEach, beforeAll: bt.beforeAll, afterAll: bt.afterAll, }); await import('@angular/compiler'); const { getTestBed, ɵgetCleanupHook } = await import('@angular/core/testing'); const { BrowserTestingModule, platformBrowserTesting } = await import('@angular/platform-browser/testing'); const { provideZonelessChangeDetection } = await import('@angular/core'); getTestBed().initTestEnvironment([BrowserTestingModule], platformBrowserTesting(), { teardown: { destroyAfterEach: true }, }); getTestBed().configureTestingModule({ providers: [provideZonelessChangeDetection()] }); bt.beforeEach(ɵgetCleanupHook(false)); bt.afterEach(ɵgetCleanupHook(true)); // Bun has a native Worker; the app must see none, as under jsdom. delete (globalThis as any).Worker; vi.stubGlobal('Worker', undefined); Object.defineProperty(window, 'matchMedia', { writable: true, value: vi.fn().mockImplementation((query: string) => ({ matches: false, media: query, onchange: null, addListener: vi.fn(), removeListener: vi.fn(), addEventListener: vi.fn(), removeEventListener: vi.fn(), dispatchEvent: vi.fn(), })), });