Something went wrong. Try again.
A WebGPU retro game engine for TypeScript, inspired by RetroBlit. Build pixel-perfect 2D games with a clean, fantasy-console-style API.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291// @vitest-environment happy-dom
/** * Unit tests for {@link bootstrap}. * * Covers the DOM-facing startup flow: * - optional waiting for `DOMContentLoaded` * - canvas lookup failures * - success and failure paths around `BTAPI.init()` * - default and custom canvas/container identifiers * - propagation of success and error callbacks * * The suite uses a minimal happy-dom document plus a mocked `BTAPI` * initialization path so bootstrap behavior can be validated without running * the full engine. */
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { BTAPI } from '../core/BTAPI';import type { IBlitTechDemo } from '../core/IBlitTechDemo';import { bootstrap } from './Bootstrap';import { DEFAULT_CANVAS_ID, DEFAULT_CONTAINER_ID } from './BootstrapHelpers';
// #region Test Demo
class MockDemo implements IBlitTechDemo { async init() { return true; }
update() {}
render() {}}
// #endregion
// #region Helpers
function setupDOM(): { canvas: HTMLCanvasElement; container: HTMLDivElement } { const container = document.createElement('div');
container.id = DEFAULT_CONTAINER_ID;
document.body.appendChild(container);
const canvas = document.createElement('canvas');
canvas.id = DEFAULT_CANVAS_ID;
document.body.appendChild(canvas);
return { canvas, container };}
function teardownDOM(): void { while (document.body.firstChild) { document.body.removeChild(document.body.firstChild); }}
// #endregion
describe('bootstrap', () => { beforeEach(() => { vi.spyOn(BTAPI.instance, 'init').mockResolvedValue(true); Object.defineProperty(document, 'readyState', { value: 'complete', writable: true, configurable: true }); });
afterEach(() => { vi.restoreAllMocks(); teardownDOM(); });
// #region waitForDOMReady
describe('waitForDOMReady', () => { it('should skip DOM waiting when waitForDOMReady is false', async () => { setupDOM();
const result = await bootstrap(MockDemo, { waitForDOMReady: false });
expect(result).toBe(true); });
it('should resolve after DOMContentLoaded when readyState is loading', async () => { Object.defineProperty(document, 'readyState', { value: 'loading', writable: true, configurable: true });
setupDOM();
const bootstrapPromise = bootstrap(MockDemo, { waitForDOMReady: true });
Object.defineProperty(document, 'readyState', { value: 'complete', writable: true, configurable: true });
document.dispatchEvent(new Event('DOMContentLoaded'));
const result = await bootstrapPromise;
expect(result).toBe(true); });
it('should proceed immediately when the readyState is complete', async () => { setupDOM();
const result = await bootstrap(MockDemo, { waitForDOMReady: true });
expect(result).toBe(true); }); });
// #endregion
// #region Backend fallback
describe('backend fallback', () => { it('should proceed to engine init when WebGPU is not supported, letting BTAPI handle fallback', async () => { setupDOM();
// No navigator.gpu installed. BTAPI.init is mocked to return true (set in beforeEach). const onError = vi.fn(); const result = await bootstrap(MockDemo, { waitForDOMReady: false, onError });
// Bootstrap no longer hard-stops on missing WebGPU; BTAPI handles backend selection. expect(result).toBe(true); expect(onError).not.toHaveBeenCalled(); }); });
// #endregion
// #region Canvas validation
describe('canvas validation', () => { it('should return false and call onError when canvas is not found', async () => { // No canvas in DOM - only the container. const container = document.createElement('div');
container.id = DEFAULT_CONTAINER_ID;
document.body.appendChild(container);
const onError = vi.fn();
const result = await bootstrap(MockDemo, { waitForDOMReady: false, canvasId: 'nonexistent-canvas', onError, });
expect(result).toBe(false); expect(onError).toHaveBeenCalledOnce(); }); });
// #endregion
// #region Engine initialization
describe('engine initialization', () => { it('should return false and call onError when demo is missing render/update', async () => { setupDOM(); const initSpy = vi.spyOn(BTAPI.instance, 'init');
class BrokenDemo implements IBlitTechDemo { async init() { return true; }
// Intentionally invalid runtime shape for beginner-mistake detection. update() {} render() {} }
(BrokenDemo.prototype as unknown as { update?: unknown }).update = undefined;
const onError = vi.fn(); const result = await bootstrap(BrokenDemo as unknown as new () => IBlitTechDemo, { waitForDOMReady: false, onError, });
expect(result).toBe(false); expect(onError).toHaveBeenCalledOnce(); expect(initSpy).not.toHaveBeenCalled(); expect(document.getElementById(DEFAULT_CONTAINER_ID)?.textContent ?? '').toContain( 'missing update() or render()', );
initSpy.mockRestore(); });
it('should return false and call onError when BTAPI.init returns false', async () => { setupDOM();
vi.spyOn(BTAPI.instance, 'init').mockResolvedValue(false);
const onError = vi.fn(); const result = await bootstrap(MockDemo, { waitForDOMReady: false, onError });
expect(result).toBe(false); expect(onError).toHaveBeenCalledOnce(); });
it('should return false and call onError when BTAPI.init throws', async () => { setupDOM();
vi.spyOn(BTAPI.instance, 'init').mockRejectedValue(new Error('Init exploded'));
const onError = vi.fn(); const result = await bootstrap(MockDemo, { waitForDOMReady: false, onError });
expect(result).toBe(false); expect(onError).toHaveBeenCalledOnce(); });
it('should return true and call onSuccess on successful initialization', async () => { setupDOM();
const onSuccess = vi.fn(); const result = await bootstrap(MockDemo, { waitForDOMReady: false, onSuccess });
expect(result).toBe(true); expect(onSuccess).toHaveBeenCalledOnce(); });
it('should set canvas tabIndex to 0 and focus the canvas before engine init', async () => { const { canvas } = setupDOM();
const focusSpy = vi.spyOn(canvas, 'focus');
await bootstrap(MockDemo, { waitForDOMReady: false });
expect(canvas.tabIndex).toBe(0); expect(focusSpy).toHaveBeenCalled(); });
it('should use default canvas and container IDs when not specified', async () => { setupDOM();
const result = await bootstrap(MockDemo, { waitForDOMReady: false });
expect(result).toBe(true); });
it('should use custom canvas and container IDs when specified', async () => { const container = document.createElement('div');
container.id = 'custom-container';
document.body.appendChild(container);
const canvas = document.createElement('canvas');
canvas.id = 'custom-canvas';
document.body.appendChild(canvas);
const result = await bootstrap(MockDemo, { waitForDOMReady: false, canvasId: 'custom-canvas', containerId: 'custom-container', });
expect(result).toBe(true); }); });
// #endregion
// #region Unexpected errors
describe('unexpected errors', () => { it('should return false when BTAPI.init throws synchronously', async () => { setupDOM();
vi.spyOn(BTAPI.instance, 'init').mockImplementation(() => { throw new Error('The synchronous throw from init'); });
const onError = vi.fn(); const result = await bootstrap(MockDemo, { waitForDOMReady: false, onError });
expect(result).toBe(false); expect(onError).toHaveBeenCalledOnce(); }); });
// #endregion});