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.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472// noinspection DuplicatedCode// @vitest-environment happy-dom
/** * Unit tests for the public `BT` facade exported from `BlitTech.ts`. * * Covers delegation from top-level `BT.*` calls into `BTAPI.instance`, * default return behavior for hardware-dependent queries, and the warning * suppression behavior used by facade helpers. */
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { BitmapFont, HardwareSettings } from './BlitTech';import { BT, Palette, Rect2i, SpriteSheet, Vector2i } from './BlitTech';import { BTAPI } from './core/BTAPI';import type { FaceButtonCode } from './input/defaultKeyboardMap';import { DEFAULT_CONTAINER_ID } from './utils/BootstrapHelpers';
// #region Helpers
const mockHardwareSettings = (displaySize = new Vector2i(320, 240), targetFPS = 60): HardwareSettings => ({ displaySize, targetFPS,});
function setupErrorContainer(): HTMLDivElement { const existingContainer = document.getElementById(DEFAULT_CONTAINER_ID);
if (existingContainer instanceof HTMLDivElement) { existingContainer.textContent = ''; return existingContainer; }
const container = document.createElement('div'); container.id = DEFAULT_CONTAINER_ID; document.body.appendChild(container); return container;}
function clearErrorContainer(): void { const container = document.getElementById(DEFAULT_CONTAINER_ID); if (container?.parentElement) { container.parentElement.removeChild(container); }}
async function withErrorContainer(callback: () => void | Promise<void>): Promise<void> { setupErrorContainer(); try { await callback(); } finally { clearErrorContainer(); }}
// #endregion
// #region BT.init
describe('BT.init', () => { beforeEach(() => { vi.restoreAllMocks(); });
it('delegates to BTAPI.instance.init and returns its result', async () => { const spy = vi.spyOn(BTAPI.instance, 'init').mockResolvedValue(true); const demo = { configure: vi.fn(), init: vi.fn(), update: vi.fn(), render: vi.fn(), }; const canvas = {} as HTMLCanvasElement;
const result = await BT.init(demo, canvas);
expect(spy).toHaveBeenCalledWith(demo, canvas); expect(result).toBe(true); });
it('forwards a failure result from BTAPI', async () => { vi.spyOn(BTAPI.instance, 'init').mockResolvedValue(false);
const demo = { configure: vi.fn(), init: vi.fn(), update: vi.fn(), render: vi.fn(), };
const result = await BT.init(demo, {} as HTMLCanvasElement);
expect(result).toBe(false); });});
// #endregion
// #region BT.displaySize
describe('BT.displaySize', () => { beforeEach(() => { vi.restoreAllMocks(); });
it('returns zero vector when hardware settings are not available', () => { vi.spyOn(BTAPI.instance, 'getHardwareSettings').mockReturnValue(null);
const size = BT.displaySize;
expect(size.x).toBe(0); expect(size.y).toBe(0); });
it('returns a clone of displaySize from hardware settings', () => { vi.spyOn(BTAPI.instance, 'getHardwareSettings').mockReturnValue(mockHardwareSettings(new Vector2i(640, 480)));
const size = BT.displaySize;
expect(size.x).toBe(640); expect(size.y).toBe(480); });});
// #endregion
// #region BT.canvasDisplaySize / BT.outputSize
describe('BT.canvasDisplaySize', () => { beforeEach(() => { vi.restoreAllMocks(); });
it('returns null when hardware settings are not available', () => { vi.spyOn(BTAPI.instance, 'getHardwareSettings').mockReturnValue(null);
expect(BT.canvasDisplaySize).toBeNull(); });
it('returns null when canvasDisplaySize was not configured', () => { vi.spyOn(BTAPI.instance, 'getHardwareSettings').mockReturnValue(mockHardwareSettings());
expect(BT.canvasDisplaySize).toBeNull(); });
it('returns a clone when canvasDisplaySize is configured', () => { vi.spyOn(BTAPI.instance, 'getHardwareSettings').mockReturnValue({ ...mockHardwareSettings(), canvasDisplaySize: new Vector2i(640, 480), });
const size = BT.canvasDisplaySize;
expect(size?.x).toBe(640); expect(size?.y).toBe(480); });
it('returns an independent clone per read', () => { vi.spyOn(BTAPI.instance, 'getHardwareSettings').mockReturnValue({ ...mockHardwareSettings(), canvasDisplaySize: new Vector2i(640, 480), });
const first = BT.canvasDisplaySize; first!.x = 999;
expect(BT.canvasDisplaySize?.x).toBe(640); });});
describe('BT.outputSize', () => { beforeEach(() => { vi.restoreAllMocks(); });
it('returns zero vector when hardware settings are not available', () => { vi.spyOn(BTAPI.instance, 'getHardwareSettings').mockReturnValue(null);
const size = BT.outputSize;
expect(size.x).toBe(0); expect(size.y).toBe(0); });
it('matches displaySize when canvasDisplaySize is omitted', () => { vi.spyOn(BTAPI.instance, 'getHardwareSettings').mockReturnValue(mockHardwareSettings(new Vector2i(320, 240)));
const size = BT.outputSize;
expect(size.x).toBe(320); expect(size.y).toBe(240); });
it('returns canvasDisplaySize when configured', () => { vi.spyOn(BTAPI.instance, 'getHardwareSettings').mockReturnValue({ ...mockHardwareSettings(new Vector2i(320, 240)), canvasDisplaySize: new Vector2i(640, 480), });
const size = BT.outputSize;
expect(size.x).toBe(640); expect(size.y).toBe(480); });
it('returns an independent clone per read', () => { vi.spyOn(BTAPI.instance, 'getHardwareSettings').mockReturnValue(mockHardwareSettings(new Vector2i(320, 240)));
const first = BT.outputSize; first.x = 999;
expect(BT.outputSize.x).toBe(320); });});
// #endregion
// #region BT.targetFPS
describe('BT.targetFPS', () => { beforeEach(() => { vi.restoreAllMocks(); });
it('returns 60 when hardware settings are not available', () => { vi.spyOn(BTAPI.instance, 'getHardwareSettings').mockReturnValue(null);
expect(BT.targetFPS).toBe(60); });
it('returns targetFPS from hardware settings', () => { vi.spyOn(BTAPI.instance, 'getHardwareSettings').mockReturnValue( mockHardwareSettings(new Vector2i(320, 240), 30), );
expect(BT.targetFPS).toBe(30); });});
// #endregion
// #region BT.deltaSeconds / BT.timeSeconds
describe('BT.deltaSeconds', () => { beforeEach(() => { vi.restoreAllMocks(); });
it('returns 1/60 when hardware settings are not available', () => { vi.spyOn(BTAPI.instance, 'getHardwareSettings').mockReturnValue(null);
expect(BT.deltaSeconds).toBeCloseTo(1 / 60); });
it('returns reciprocal of targetFPS from hardware settings', () => { vi.spyOn(BTAPI.instance, 'getHardwareSettings').mockReturnValue( mockHardwareSettings(new Vector2i(320, 240), 50), );
expect(BT.deltaSeconds).toBeCloseTo(0.02); });
it('falls back to 1/60 when targetFPS is non-positive', () => { vi.spyOn(BTAPI.instance, 'getHardwareSettings').mockReturnValue( mockHardwareSettings(new Vector2i(320, 240), 0), );
expect(BT.deltaSeconds).toBeCloseTo(1 / 60); });
it('falls back to 1/60 when targetFPS is non-finite', () => { vi.spyOn(BTAPI.instance, 'getHardwareSettings').mockReturnValue( mockHardwareSettings(new Vector2i(320, 240), Number.NaN), );
expect(BT.deltaSeconds).toBeCloseTo(1 / 60); });});
describe('BT.timeSeconds', () => { beforeEach(() => { vi.restoreAllMocks(); });
it('returns ticks multiplied by deltaSeconds', () => { vi.spyOn(BTAPI.instance, 'getTicks').mockReturnValue(90); vi.spyOn(BTAPI.instance, 'getHardwareSettings').mockReturnValue( mockHardwareSettings(new Vector2i(320, 240), 30), );
expect(BT.timeSeconds).toBeCloseTo(3); });
it('uses fallback delta when targetFPS is non-positive, producing finite time', () => { vi.spyOn(BTAPI.instance, 'getTicks').mockReturnValue(120); vi.spyOn(BTAPI.instance, 'getHardwareSettings').mockReturnValue( mockHardwareSettings(new Vector2i(320, 240), 0), );
const time = BT.timeSeconds; expect(BT.deltaSeconds).toBeCloseTo(1 / 60); expect(Number.isFinite(time)).toBe(true); expect(time).toBeCloseTo(2); });
it('uses fallback delta when targetFPS is non-finite, producing finite time', () => { vi.spyOn(BTAPI.instance, 'getTicks').mockReturnValue(120); vi.spyOn(BTAPI.instance, 'getHardwareSettings').mockReturnValue( mockHardwareSettings(new Vector2i(320, 240), Number.POSITIVE_INFINITY), );
const time = BT.timeSeconds; expect(BT.deltaSeconds).toBeCloseTo(1 / 60); expect(Number.isFinite(time)).toBe(true); expect(time).toBeCloseTo(2); });});
// #endregion
// #region BT.ticks / BT.ticksReset
describe('BT.ticks', () => { beforeEach(() => { vi.restoreAllMocks(); });
it('delegates to BTAPI.instance.getTicks', () => { vi.spyOn(BTAPI.instance, 'getTicks').mockReturnValue(42);
expect(BT.ticks).toBe(42); });});
describe('BT.ticksReset', () => { beforeEach(() => { vi.restoreAllMocks(); });
it('delegates to BTAPI.instance.resetTicks', () => { const spy = vi.spyOn(BTAPI.instance, 'resetTicks').mockReturnValue(undefined);
BT.ticksReset();
expect(spy).toHaveBeenCalledOnce(); });});
// #endregion
// #region BT.paletteCreate / BT.paletteSet / BT.palette
describe('BT.paletteCreate', () => { it('creates a palette with the requested size', () => { const palette = BT.paletteCreate(16);
expect(palette).toBeInstanceOf(Palette); expect(palette.size).toBe(16); });});
describe('BT.paletteSet', () => { beforeEach(() => { vi.restoreAllMocks(); });
it('delegates to BTAPI.instance.setPalette', () => { const spy = vi.spyOn(BTAPI.instance, 'setPalette').mockReturnValue(undefined); const palette = new Palette(16);
BT.paletteSet(palette);
expect(spy).toHaveBeenCalledWith(palette); });
it('shows a beginner-friendly error when passed undefined', async () => { await withErrorContainer(async () => { BT.paletteSet(undefined as never);
const text = document.getElementById(DEFAULT_CONTAINER_ID)?.textContent ?? ''; expect(text).toContain('BT.paletteSet expects a Palette'); }); });
it('shows a beginner-friendly error when passed null', async () => { await withErrorContainer(async () => { BT.paletteSet(null as never);
const text = document.getElementById(DEFAULT_CONTAINER_ID)?.textContent ?? ''; expect(text).toContain('BT.paletteSet expects a Palette'); }); });});
describe('BT.palette', () => { beforeEach(() => { vi.restoreAllMocks(); });
it('returns palette when one is set', () => { const palette = new Palette(16);
vi.spyOn(BTAPI.instance, 'getPalette').mockReturnValue(palette);
expect(BT.palette).toBe(palette); });
it('throws when no palette is set', () => { vi.spyOn(BTAPI.instance, 'getPalette').mockReturnValue(null);
expect(() => BT.palette).toThrow('No palette set yet. Call BT.paletteSet'); });});
// #endregion
// #region BT.clear / BT.clearRect
describe('BT.clear', () => { beforeEach(() => { vi.restoreAllMocks(); vi.spyOn(BTAPI.instance, 'getRenderer').mockReturnValue({} as never); });
it('delegates to BTAPI.instance.setClearColor', () => { const spy = vi.spyOn(BTAPI.instance, 'setClearColor').mockReturnValue(undefined);
BT.clear(1);
expect(spy).toHaveBeenCalledWith(1); });});
describe('BT.clearRect', () => { beforeEach(() => { vi.restoreAllMocks(); vi.spyOn(BTAPI.instance, 'getRenderer').mockReturnValue({} as never); });
it('delegates to BTAPI.instance.clearRect', () => { const spy = vi.spyOn(BTAPI.instance, 'clearRect').mockReturnValue(undefined); const rect = new Rect2i(0, 0, 100, 100);
BT.clearRect(rect, 1);
expect(spy).toHaveBeenCalledWith(rect, 1); });});
// #endregion
// #region BT.drawPixel / BT.drawLine / BT.drawRect / BT.drawRectFill
describe('BT.drawPixel', () => { beforeEach(() => { vi.restoreAllMocks(); vi.spyOn(BTAPI.instance, 'getRenderer').mockReturnValue({} as never); });
it('delegates to BTAPI.instance.drawPixel', () => { const spy = vi.spyOn(BTAPI.instance, 'drawPixel').mockReturnValue(undefined); const pos = new Vector2i(5, 10);
BT.drawPixel(pos, 2);
expect(spy).toHaveBeenCalledWith(pos, 2); });
it('supports (x, y, color) arguments', () => { const spy = vi.spyOn(BTAPI.instance, 'drawPixel').mockReturnValue(undefined);
BT.drawPixel(5, 10, 3);
expect(spy).toHaveBeenCalledWith(expect.objectContaining({ x: 5, y: 10 }), 3); });
it('shows a beginner-friendly error when drawPixel arguments are invalid', async () => { await withErrorContainer(async () => { BT.drawPixel({} as never, 2 as never);
const text = document.getElementById(DEFAULT_CONTAINER_ID)?.textContent ?? ''; expect(text).toContain('drawPixel expects (x, y, paletteIndex) or (Vector2i, paletteIndex).'); }); });});
describe('BT.drawLine', () => { beforeEach(() => { vi.restoreAllMocks(); vi.spyOn(BTAPI.instance, 'getRenderer').mockReturnValue({} as never); });
it('delegates to BTAPI.instance.drawLine', () => { const spy = vi.spyOn(BTAPI.instance, 'drawLine').mockReturnValue(undefined); const p0 = new Vector2i(0, 0); const p1 = new Vector2i(50, 50);
BT.drawLine(p0, p1, 3);
expect(spy).toHaveBeenCalledWith(p0, p1, 3); });});
describe('BT.drawRect', () => { beforeEach(() => { vi.restoreAllMocks(); vi.spyOn(BTAPI.instance, 'getRenderer').mockReturnValue({} as never); });
it('delegates to BTAPI.instance.drawRect', () => { const spy = vi.spyOn(BTAPI.instance, 'drawRect').mockReturnValue(undefined); const rect = new Rect2i(10, 10, 40, 30);
BT.drawRect(rect, 4);
expect(spy).toHaveBeenCalledWith(rect, 4); });});
describe('BT.drawRectFill', () => { beforeEach(() => { vi.restoreAllMocks(); vi.spyOn(BTAPI.instance, 'getRenderer').mockReturnValue({} as never); });
it('delegates to BTAPI.instance.drawRectFill', () => { const spy = vi.spyOn(BTAPI.instance, 'drawRectFill').mockReturnValue(undefined); const rect = new Rect2i(0, 0, 20, 20);
BT.drawRectFill(rect, 8);
expect(spy).toHaveBeenCalledWith(rect, 8); });});
// #endregion
// #region BT.cameraSet / BT.camera / BT.cameraClamp / BT.cameraReset
describe('BT.cameraSet', () => { beforeEach(() => { vi.restoreAllMocks(); });
it('delegates to BTAPI.instance.setCameraOffset', () => { const spy = vi.spyOn(BTAPI.instance, 'setCameraOffset').mockReturnValue(undefined); const offset = new Vector2i(100, 50);
BT.cameraSet(offset);
expect(spy).toHaveBeenCalledWith(offset); });});
describe('BT.camera', () => { beforeEach(() => { vi.restoreAllMocks(); });
it('delegates to BTAPI.instance.getCameraOffset and returns its result', () => { const expected = new Vector2i(64, 32); vi.spyOn(BTAPI.instance, 'getCameraOffset').mockReturnValue(expected);
const result = BT.camera;
expect(result).toBe(expected); });
it('returns an independent clone per read', () => { const stored = new Vector2i(64, 32); vi.spyOn(BTAPI.instance, 'getCameraOffset').mockImplementation(() => stored.clone());
const first = BT.camera; first.x = 999;
expect(BT.camera.x).toBe(64); });});
describe('BT.cameraClamp', () => { beforeEach(() => { vi.restoreAllMocks(); });
it('clamps camera coordinates when viewSize is provided', () => { const clamped = BT.cameraClamp(new Vector2i(500, 300), new Vector2i(640, 480), new Vector2i(320, 240));
expect(clamped.equalsXY(320, 240)).toBe(true); });
it('uses BT.displaySize when viewSize is omitted', () => { vi.spyOn(BTAPI.instance, 'getHardwareSettings').mockReturnValue( mockHardwareSettings(new Vector2i(200, 150), 60), );
const clamped = BT.cameraClamp(new Vector2i(100, 100), new Vector2i(250, 200));
expect(clamped.equalsXY(50, 50)).toBe(true); });});
describe('BT.cameraReset', () => { beforeEach(() => { vi.restoreAllMocks(); });
it('delegates to BTAPI.instance.resetCamera', () => { const spy = vi.spyOn(BTAPI.instance, 'resetCamera').mockReturnValue(undefined);
BT.cameraReset();
expect(spy).toHaveBeenCalledOnce(); });});
// #endregion
// #region BT.buttonDown / BT.buttonPressed / BT.buttonReleased
describe('BT.buttonDown', () => { beforeEach(() => { BT.inputMapReset(); });
afterEach(() => { vi.restoreAllMocks(); BT.inputMapReset(); });
it('returns false for face buttons when keyboard and gamepad are unavailable', () => { expect(BT.buttonDown(BT.BTN_A)).toBe(false); });
it('accepts an optional player index without throwing', () => { expect(BT.buttonDown(BT.BTN_START, 2)).toBe(false); });
it('delegates pointer buttons to the pointer subsystem', () => { const isButtonDown = vi.fn().mockReturnValue(true); vi.spyOn(BTAPI.instance, 'getPointer').mockReturnValue({ isButtonDown } as never);
expect(BT.buttonDown(BT.BTN_POINTER_A, 0)).toBe(true); expect(isButtonDown).toHaveBeenCalledWith(20, 0); });
it('uses ANY semantics for combined button masks', () => { const isButtonDown = vi.fn().mockImplementation((codes: readonly string[]) => codes.includes('KeyA')); vi.spyOn(BTAPI.instance, 'getKeyboard').mockReturnValue({ isButtonDown } as never);
BT.inputMap(0, BT.BTN_A, 'KeyA');
expect(BT.buttonDown(BT.BTN_A | BT.BTN_B, 0)).toBe(true); });
it('returns false for pointer buttons when the engine is not initialized', () => { vi.spyOn(BTAPI.instance, 'getPointer').mockReturnValue(null);
expect(BT.buttonDown(BT.BTN_POINTER_A)).toBe(false); expect(BT.buttonDown(BT.BTN_POINTER_D, 3)).toBe(false); });
it('uses gamepad for player 2+ when keyboard maps are unavailable', () => { const isButtonDown = vi.fn().mockReturnValue(true); vi.spyOn(BTAPI.instance, 'getGamepad').mockReturnValue({ isButtonDown } as never);
expect(BT.buttonDown(BT.BTN_START, 2)).toBe(true); expect(isButtonDown).toHaveBeenCalledWith(BT.BTN_START, 2); });});
describe('BT.buttonPressed', () => { afterEach(() => { vi.restoreAllMocks(); });
it('returns false for face buttons when keyboard and gamepad are unavailable', () => { expect(BT.buttonPressed(BT.BTN_B)).toBe(false); });
it('accepts an optional player index without throwing', () => { expect(BT.buttonPressed(BT.BTN_B, 1)).toBe(false); });
it('delegates pointer buttons to the pointer subsystem', () => { const isButtonPressed = vi.fn().mockReturnValue(true); vi.spyOn(BTAPI.instance, 'getPointer').mockReturnValue({ isButtonPressed } as never);
expect(BT.buttonPressed(BT.BTN_POINTER_B, 0)).toBe(true); expect(isButtonPressed).toHaveBeenCalledWith(21, 0); });
it('forwards repeatRate to keyboard/gamepad face-button queries', () => { vi.spyOn(BTAPI.instance, 'getTicks').mockReturnValue(120); const isButtonPressedKeyboard = vi.fn().mockReturnValue(false); const isButtonPressedGamepad = vi.fn().mockReturnValue(true); const isButtonDownKeyboard = vi.fn().mockReturnValue(false); const isButtonDownGamepad = vi.fn().mockReturnValue(false); vi.spyOn(BTAPI.instance, 'getKeyboard').mockReturnValue({ isButtonDown: isButtonDownKeyboard, isButtonPressed: isButtonPressedKeyboard, } as never); vi.spyOn(BTAPI.instance, 'getGamepad').mockReturnValue({ isButtonDown: isButtonDownGamepad, isButtonPressed: isButtonPressedGamepad, } as never);
BT.buttonPressed(BT.BTN_A, 0, 6);
expect(isButtonPressedKeyboard).toHaveBeenCalledWith(expect.any(Array), 6, 120); expect(isButtonPressedGamepad).toHaveBeenCalledWith(BT.BTN_A, 0, 6, 120); });
it('suppresses pressed edge when merged button was already down via keyboard', () => { vi.spyOn(BTAPI.instance, 'getTicks').mockReturnValue(120); vi.spyOn(BTAPI.instance, 'getKeyboard').mockReturnValue({ isButtonDown: vi.fn().mockReturnValue(true), isButtonPressed: vi.fn().mockReturnValue(false), } as never); vi.spyOn(BTAPI.instance, 'getGamepad').mockReturnValue({ isButtonDown: vi.fn().mockReturnValue(true), isButtonPressed: vi.fn().mockReturnValue(true), } as never);
expect(BT.buttonPressed(BT.BTN_A, 0)).toBe(false); });
it('allows repeat events during dual-source hold when repeatRate is enabled', () => { vi.spyOn(BTAPI.instance, 'getTicks').mockReturnValue(120); vi.spyOn(BTAPI.instance, 'getKeyboard').mockReturnValue({ isButtonDown: vi.fn().mockReturnValue(true), isButtonPressed: vi.fn().mockReturnValue(true), } as never); vi.spyOn(BTAPI.instance, 'getGamepad').mockReturnValue({ isButtonDown: vi.fn().mockReturnValue(true), isButtonPressed: vi.fn().mockReturnValue(false), } as never);
expect(BT.buttonPressed(BT.BTN_A, 0, 6)).toBe(true); });});
describe('BT.buttonReleased', () => { afterEach(() => { vi.restoreAllMocks(); });
it('returns false for face buttons when keyboard is unavailable', () => { expect(BT.buttonReleased(BT.BTN_X)).toBe(false); });
it('accepts an optional player index without throwing', () => { expect(BT.buttonReleased(BT.BTN_X, 3)).toBe(false); });
it('delegates pointer buttons to the pointer subsystem', () => { const isButtonReleased = vi.fn().mockReturnValue(true); vi.spyOn(BTAPI.instance, 'getPointer').mockReturnValue({ isButtonReleased } as never);
expect(BT.buttonReleased(BT.BTN_POINTER_C, 2)).toBe(true); expect(isButtonReleased).toHaveBeenCalledWith(22, 2); });
it('merges keyboard and gamepad for players 0 and 1', () => { const isButtonReleasedKeyboard = vi.fn().mockReturnValue(false); const isButtonReleased = vi.fn().mockReturnValue(true); const isButtonDownKeyboard = vi.fn().mockReturnValue(false); const isButtonDown = vi.fn().mockReturnValue(false); vi.spyOn(BTAPI.instance, 'getKeyboard').mockReturnValue({ isButtonDown: isButtonDownKeyboard, isButtonReleased: isButtonReleasedKeyboard, } as never); vi.spyOn(BTAPI.instance, 'getGamepad').mockReturnValue({ isButtonDown, isButtonReleased } as never);
expect(BT.buttonReleased(BT.BTN_A, 1)).toBe(true); expect(isButtonReleased).toHaveBeenCalledWith(BT.BTN_A, 1); });
it('suppresses released edge when merged button remains down via keyboard', () => { vi.spyOn(BTAPI.instance, 'getKeyboard').mockReturnValue({ isButtonDown: vi.fn().mockReturnValue(true), isButtonReleased: vi.fn().mockReturnValue(false), } as never); vi.spyOn(BTAPI.instance, 'getGamepad').mockReturnValue({ isButtonDown: vi.fn().mockReturnValue(false), isButtonReleased: vi.fn().mockReturnValue(true), } as never);
expect(BT.buttonReleased(BT.BTN_A, 0)).toBe(false); });});
// #endregion
// #region BT.inputMap / BT.inputMapReset
describe('BT.inputMap / BT.inputMapReset', () => { beforeEach(() => { vi.restoreAllMocks(); BT.inputMapReset(); });
it('remaps player 0 face buttons through the keyboard subsystem', () => { const isButtonDown = vi.fn().mockReturnValue(true); vi.spyOn(BTAPI.instance, 'getKeyboard').mockReturnValue({ isButtonDown } as never);
BT.inputMap(0, BT.BTN_A, 'KeyZ');
expect(BT.buttonDown(BT.BTN_A, 0)).toBe(true); expect(isButtonDown).toHaveBeenCalledWith(['KeyZ']); });
it('remaps player 1 independently of player 0', () => { const isButtonDown = vi.fn().mockImplementation((codes: readonly string[]) => codes.includes('Numpad9')); vi.spyOn(BTAPI.instance, 'getKeyboard').mockReturnValue({ isButtonDown } as never);
BT.inputMap(0, BT.BTN_A, 'KeyX'); BT.inputMap(1, BT.BTN_A, 'Numpad9');
BT.buttonDown(BT.BTN_A, 0); expect(isButtonDown).toHaveBeenLastCalledWith(['KeyX']);
BT.buttonDown(BT.BTN_A, 1); expect(isButtonDown).toHaveBeenLastCalledWith(['Numpad9']); });
it('passes multiple keys per button for OR semantics', () => { const isButtonDown = vi.fn().mockReturnValue(true); vi.spyOn(BTAPI.instance, 'getKeyboard').mockReturnValue({ isButtonDown } as never);
BT.inputMap(0, BT.BTN_UP, 'ArrowUp', 'KeyW');
BT.buttonDown(BT.BTN_UP, 0); expect(isButtonDown).toHaveBeenCalledWith(['ArrowUp', 'KeyW']); });
it('restores default key lists for both keyboard players', () => { const isButtonDown = vi.fn().mockReturnValue(false); vi.spyOn(BTAPI.instance, 'getKeyboard').mockReturnValue({ isButtonDown } as never);
BT.inputMap(0, BT.BTN_UP, 'F1'); BT.inputMap(1, BT.BTN_UP, 'F2'); BT.inputMapReset();
BT.buttonDown(BT.BTN_UP, 0); expect(isButtonDown).toHaveBeenCalledWith([ ...(BT.DEFAULT_KEYBOARD_PLAYER1[BT.BTN_UP as FaceButtonCode] ?? []), ]);
BT.buttonDown(BT.BTN_UP, 1); expect(isButtonDown).toHaveBeenCalledWith([ ...(BT.DEFAULT_KEYBOARD_PLAYER2[BT.BTN_UP as FaceButtonCode] ?? []), ]); });
it('ignores remap attempts for unsupported keyboard players', () => { const isButtonDown = vi.fn().mockReturnValue(false); vi.spyOn(BTAPI.instance, 'getKeyboard').mockReturnValue({ isButtonDown } as never);
BT.inputMap(2, BT.BTN_A, 'KeyZ');
BT.buttonDown(BT.BTN_A, 0); expect(isButtonDown).toHaveBeenCalledWith([...(BT.DEFAULT_KEYBOARD_PLAYER1[BT.BTN_A as FaceButtonCode] ?? [])]); });
it('ignores remap attempts with out-of-range face button ids', () => { const isButtonDown = vi.fn().mockReturnValue(false); vi.spyOn(BTAPI.instance, 'getKeyboard').mockReturnValue({ isButtonDown } as never);
BT.inputMap(0, 99, 'KeyZ');
BT.buttonDown(BT.BTN_A, 0); expect(isButtonDown).toHaveBeenCalledWith([...(BT.DEFAULT_KEYBOARD_PLAYER1[BT.BTN_A as FaceButtonCode] ?? [])]); });
it('allows clearing keyboard bindings with an empty key list', () => { const isButtonDown = vi.fn().mockReturnValue(false); vi.spyOn(BTAPI.instance, 'getKeyboard').mockReturnValue({ isButtonDown } as never);
BT.inputMap(0, BT.BTN_A);
BT.buttonDown(BT.BTN_A, 0); expect(isButtonDown).toHaveBeenCalledWith([]); });});
// #endregion
// #region BT pointer queries
describe('BT.pointerPos', () => { afterEach(() => { vi.restoreAllMocks(); });
it('returns Vector2i.zero() when the engine is not initialized', () => { vi.spyOn(BTAPI.instance, 'getPointer').mockReturnValue(null);
const pos = BT.pointerPos();
expect(pos.x).toBe(0); expect(pos.y).toBe(0); });
it('delegates to the pointer subsystem with the supplied slot index', () => { const expected = new Vector2i(80, 60); const getPos = vi.fn().mockReturnValue(expected); vi.spyOn(BTAPI.instance, 'getPointer').mockReturnValue({ getPos } as never);
const pos = BT.pointerPos(2);
expect(getPos).toHaveBeenCalledWith(2); expect(pos).toBe(expected); });
it('defaults the pointer index to 0 (mouse)', () => { const getPos = vi.fn().mockReturnValue(new Vector2i()); vi.spyOn(BTAPI.instance, 'getPointer').mockReturnValue({ getPos } as never);
BT.pointerPos();
expect(getPos).toHaveBeenCalledWith(0); });});
describe('BT.pointerDelta', () => { afterEach(() => { vi.restoreAllMocks(); });
it('returns Vector2i.zero() when the engine is not initialized', () => { vi.spyOn(BTAPI.instance, 'getPointer').mockReturnValue(null);
const delta = BT.pointerDelta();
expect(delta.x).toBe(0); expect(delta.y).toBe(0); });
it('delegates to the pointer subsystem', () => { const expected = new Vector2i(5, -3); const getDelta = vi.fn().mockReturnValue(expected); vi.spyOn(BTAPI.instance, 'getPointer').mockReturnValue({ getDelta } as never);
expect(BT.pointerDelta(1)).toBe(expected); expect(getDelta).toHaveBeenCalledWith(1); });});
describe('BT.pointerPosValid', () => { afterEach(() => { vi.restoreAllMocks(); });
it('returns false when the engine is not initialized', () => { vi.spyOn(BTAPI.instance, 'getPointer').mockReturnValue(null);
expect(BT.pointerPosValid()).toBe(false); });
it('delegates to the pointer subsystem', () => { const isValid = vi.fn().mockReturnValue(true); vi.spyOn(BTAPI.instance, 'getPointer').mockReturnValue({ isValid } as never);
expect(BT.pointerPosValid(0)).toBe(true); expect(isValid).toHaveBeenCalledWith(0); });});
describe('BT.pointerScrollDelta', () => { afterEach(() => { vi.restoreAllMocks(); });
it('returns 0 when the engine is not initialized', () => { vi.spyOn(BTAPI.instance, 'getPointer').mockReturnValue(null);
expect(BT.pointerScrollDelta).toBe(0); });
it('delegates to the pointer subsystem', () => { const getScrollDelta = vi.fn().mockReturnValue(42); vi.spyOn(BTAPI.instance, 'getPointer').mockReturnValue({ getScrollDelta } as never);
expect(BT.pointerScrollDelta).toBe(42); });});
// #endregion
// #region Pointer button constants
describe('Pointer button constants', () => { it('exposes BTN_POINTER_A..D as bit flags', () => { expect(BT.BTN_POINTER_A).toBe(1 << 12); expect(BT.BTN_POINTER_B).toBe(1 << 13); expect(BT.BTN_POINTER_C).toBe(1 << 14); expect(BT.BTN_POINTER_D).toBe(1 << 15); });});
// #endregion
// #region BT gamepad constants and APIs / BT.keyDown / BT.keyPressed / BT.keyReleased
describe('BT gamepad constants and APIs', () => { afterEach(() => { vi.restoreAllMocks(); });
it('exposes player constants', () => { expect(BT.PLAYER_ONE).toBe(0); expect(BT.PLAYER_TWO).toBe(1); expect(BT.PLAYER_THREE).toBe(2); expect(BT.PLAYER_FOUR).toBe(3); });
it('exposes axis constants', () => { expect(BT.AXIS_LEFT_X).toBe(0); expect(BT.AXIS_LEFT_Y).toBe(1); expect(BT.AXIS_RIGHT_X).toBe(2); expect(BT.AXIS_RIGHT_Y).toBe(3); expect(BT.AXIS_TRIGGER_L).toBe(4); expect(BT.AXIS_TRIGGER_R).toBe(5); });
it('exposes combo constants', () => { expect(BT.BTN_ABXY).toBe(BT.BTN_A | BT.BTN_B | BT.BTN_X | BT.BTN_Y); expect(BT.BTN_SHOULDER).toBe(BT.BTN_L | BT.BTN_R); expect(BT.BTN_POINTER_ANY).toBe(BT.BTN_POINTER_A | BT.BTN_POINTER_B | BT.BTN_POINTER_C | BT.BTN_POINTER_D); });
it('delegates getAxis/gamepadConnected/gamepadCount to gamepad subsystem', () => { const getAxis = vi.fn().mockReturnValue(0.25); const isConnected = vi.fn().mockReturnValue(true); const connectedCount = vi.fn().mockReturnValue(2); vi.spyOn(BTAPI.instance, 'getGamepad').mockReturnValue({ getAxis, isConnected, connectedCount } as never);
expect(BT.getAxis(BT.AXIS_LEFT_X, 1)).toBe(0.25); expect(getAxis).toHaveBeenCalledWith(BT.AXIS_LEFT_X, 1); expect(BT.gamepadConnected(1)).toBe(true); expect(isConnected).toHaveBeenCalledWith(1); expect(BT.gamepadCount).toBe(2); expect(connectedCount).toHaveBeenCalledWith(); });
it('returns 0 from getAxis when the gamepad subsystem is not initialized', () => { vi.spyOn(BTAPI.instance, 'getGamepad').mockReturnValue(null);
expect(BT.getAxis(BT.AXIS_LEFT_X, 1)).toBe(0); });
it('returns false from gamepadConnected when the gamepad subsystem is not initialized', () => { vi.spyOn(BTAPI.instance, 'getGamepad').mockReturnValue(null);
expect(BT.gamepadConnected(1)).toBe(false); });
it('returns 0 from gamepadCount when the gamepad subsystem is not initialized', () => { vi.spyOn(BTAPI.instance, 'getGamepad').mockReturnValue(null);
expect(BT.gamepadCount).toBe(0); });});
describe('BT.keyDown', () => { it('returns false when the engine is not initialized', () => { expect(BT.keyDown('Space')).toBe(false); });});
describe('BT.keyPressed', () => { it('returns false when the engine is not initialized', () => { expect(BT.keyPressed('ArrowUp')).toBe(false); });});
describe('BT.keyReleased', () => { it('returns false when the engine is not initialized', () => { expect(BT.keyReleased('KeyA')).toBe(false); });});
// #endregion
// #region BT.systemPrint / BT.systemPrintMeasure / BT.printFont
describe('BT.systemPrint', () => { beforeEach(() => { vi.restoreAllMocks(); vi.spyOn(BTAPI.instance, 'getRenderer').mockReturnValue({} as never); });
it('delegates to BTAPI.instance.drawSystemText', () => { const spy = vi.spyOn(BTAPI.instance, 'drawSystemText').mockReturnValue(undefined); const pos = new Vector2i(10, 10);
BT.systemPrint(pos, 8, 'Hello');
expect(spy).toHaveBeenCalledWith(pos, 8, 'Hello'); });});
describe('BT.systemPrintMeasure', () => { it('returns zero vector when system font is not initialized', () => { vi.spyOn(BTAPI.instance, 'getSystemFont').mockReturnValue(null);
const result = BT.systemPrintMeasure('any text');
expect(result.x).toBe(0); expect(result.y).toBe(0); });
it('returns measured size when system font is available', () => { const mockFont = { measureTextSize: vi.fn().mockReturnValue({ width: 40, height: 8 }), };
vi.spyOn(BTAPI.instance, 'getSystemFont').mockReturnValue(mockFont as unknown as BitmapFont);
const result = BT.systemPrintMeasure('Hello');
expect(result.x).toBe(40); expect(result.y).toBe(8); expect(mockFont.measureTextSize).toHaveBeenCalledWith('Hello'); });});
describe('BT.printFont', () => { beforeEach(() => { vi.restoreAllMocks(); vi.spyOn(BTAPI.instance, 'getRenderer').mockReturnValue({} as never); });
it('delegates to BTAPI.instance.drawBitmapText', () => { const spy = vi.spyOn(BTAPI.instance, 'drawBitmapText').mockReturnValue(undefined); const font = {} as BitmapFont; const pos = new Vector2i(0, 0); const paletteOffset = 4;
BT.printFont(font, pos, 'Hi', paletteOffset);
expect(spy).toHaveBeenCalledWith(font, pos, 'Hi', paletteOffset); });
it('forwards an undefined paletteOffset when omitted', () => { const spy = vi.spyOn(BTAPI.instance, 'drawBitmapText').mockReturnValue(undefined); const font = {} as BitmapFont;
BT.printFont(font, new Vector2i(0, 0), 'Hi');
expect(spy).toHaveBeenCalledWith(font, expect.anything(), 'Hi', undefined); });
it('shows a missing await message for Promise font values', async () => { await withErrorContainer(async () => { BT.printFont(Promise.resolve({}) as unknown as BitmapFont, new Vector2i(0, 0), 'Hi');
const text = document.getElementById(DEFAULT_CONTAINER_ID)?.textContent ?? ''; expect(text).toContain("Did you forget to use 'await' before BitmapFont.load()?"); }); });});
// #endregion
// #region BT.captureFrame / BT.downloadFrame
describe('BT.captureFrame', () => { beforeEach(() => { vi.restoreAllMocks(); });
it('delegates to BTAPI.instance.captureFrame and returns its result', async () => { const mockBlob = new Blob(['test'], { type: 'image/png' }); const spy = vi.spyOn(BTAPI.instance, 'captureFrame').mockResolvedValue(mockBlob);
const result = await BT.captureFrame();
expect(spy).toHaveBeenCalledOnce(); expect(result).toBe(mockBlob); });});
describe('BT.downloadFrame', () => { beforeEach(() => { vi.restoreAllMocks(); });
afterEach(() => { vi.unstubAllGlobals(); });
it('calls captureFrame, creates object URL, clicks anchor, and revokes URL', async () => { const mockBlob = new Blob(['test'], { type: 'image/png' });
vi.spyOn(BTAPI.instance, 'captureFrame').mockResolvedValue(mockBlob);
const mockUrl = 'blob:mock-url'; const createObjectURL = vi.fn().mockReturnValue(mockUrl); const revokeObjectURL = vi.fn();
vi.stubGlobal('URL', { createObjectURL, revokeObjectURL });
const mockAnchor = { href: '', download: '', click: vi.fn() };
vi.spyOn(document, 'createElement').mockReturnValue(mockAnchor as unknown as HTMLAnchorElement);
await BT.downloadFrame('screenshot.png');
expect(createObjectURL).toHaveBeenCalledWith(mockBlob); expect(mockAnchor.href).toBe(mockUrl); expect(mockAnchor.download).toBe('screenshot.png'); expect(mockAnchor.click).toHaveBeenCalledOnce(); expect(revokeObjectURL).toHaveBeenCalledWith(mockUrl); });
it('uses the default filename when none is provided', async () => { const mockBlob = new Blob(['test'], { type: 'image/png' });
vi.spyOn(BTAPI.instance, 'captureFrame').mockResolvedValue(mockBlob); vi.stubGlobal('URL', { createObjectURL: vi.fn().mockReturnValue('blob:x'), revokeObjectURL: vi.fn() });
const mockAnchor = { href: '', download: '', click: vi.fn() };
vi.spyOn(document, 'createElement').mockReturnValue(mockAnchor as unknown as HTMLAnchorElement);
await BT.downloadFrame();
expect(mockAnchor.download).toBe('blit-tech-capture.png'); });});
// #endregion
// #region BT.drawSprite
describe('BT.drawSprite', () => { const mockImage = { width: 64, height: 64 } as HTMLImageElement;
beforeEach(() => { vi.restoreAllMocks(); vi.spyOn(BTAPI.instance, 'getRenderer').mockReturnValue({} as never); });
it('delegates to BTAPI.instance.drawSprite', () => { const spy = vi.spyOn(BTAPI.instance, 'drawSprite').mockReturnValue(undefined); const sheet = new SpriteSheet(mockImage); const srcRect = new Rect2i(0, 0, 16, 16); const destPos = new Vector2i(10, 20);
BT.drawSprite(sheet, srcRect, destPos);
expect(spy).toHaveBeenCalledOnce(); expect(spy).toHaveBeenCalledWith(sheet, srcRect, destPos, undefined); });
it('forwards the paletteOffset argument', () => { const spy = vi.spyOn(BTAPI.instance, 'drawSprite').mockReturnValue(undefined); const sheet = new SpriteSheet(mockImage); const paletteOffset = 16;
BT.drawSprite(sheet, new Rect2i(0, 0, 16, 16), new Vector2i(0, 0), paletteOffset);
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), expect.anything(), paletteOffset); });
it('forwards undefined tint when omitted', () => { const spy = vi.spyOn(BTAPI.instance, 'drawSprite').mockReturnValue(undefined); const sheet = new SpriteSheet(mockImage);
BT.drawSprite(sheet, new Rect2i(0, 0, 8, 8), new Vector2i(0, 0));
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), expect.anything(), undefined); });
it('does not throw when called multiple times', () => { vi.spyOn(BTAPI.instance, 'drawSprite').mockReturnValue(undefined);
const sheet = new SpriteSheet(mockImage);
expect(() => { BT.drawSprite(sheet, new Rect2i(0, 0, 16, 16), new Vector2i(0, 0)); BT.drawSprite(sheet, new Rect2i(16, 0, 16, 16), new Vector2i(20, 0)); }).not.toThrow(); });
it('shows a missing await message for Promise sprite sheet values', async () => { await withErrorContainer(async () => { BT.drawSprite(Promise.resolve({}) as unknown as SpriteSheet, new Rect2i(0, 0, 8, 8), new Vector2i(0, 0), 0);
const text = document.getElementById(DEFAULT_CONTAINER_ID)?.textContent ?? ''; expect(text).toContain("Did you forget to use 'await' before SpriteSheet.load()?"); }); });
it('shows engine-not-ready message when drawing before bootstrap completes', async () => { await withErrorContainer(async () => { vi.spyOn(BTAPI.instance, 'getRenderer').mockReturnValue(null);
BT.drawSprite(new SpriteSheet(mockImage), new Rect2i(0, 0, 8, 8), new Vector2i(0, 0), 0);
const text = document.getElementById(DEFAULT_CONTAINER_ID)?.textContent ?? ''; expect(text).toContain("The engine isn't ready yet."); }); });});
// #endregion
// #region BT.effectAdd / BT.effectRemove / BT.effectClear
describe('BT.effectAdd / BT.effectRemove / BT.effectClear', () => { function makeStubEffect() { return { tier: 'pixel' as const, init: vi.fn(), updateUniforms: vi.fn(), encodePass: vi.fn(), dispose: vi.fn(), }; }
beforeEach(() => { vi.restoreAllMocks(); });
it('effectAdd shows engine-not-ready when called before bootstrap', async () => { await withErrorContainer(async () => { vi.spyOn(BTAPI.instance, 'getRenderer').mockReturnValue(null);
BT.effectAdd(makeStubEffect());
const text = document.getElementById(DEFAULT_CONTAINER_ID)?.textContent ?? ''; expect(text).toContain("The engine isn't ready yet."); }); });
it('effectRemove shows engine-not-ready when called before bootstrap', async () => { await withErrorContainer(async () => { vi.spyOn(BTAPI.instance, 'getRenderer').mockReturnValue(null);
BT.effectRemove(makeStubEffect());
const text = document.getElementById(DEFAULT_CONTAINER_ID)?.textContent ?? ''; expect(text).toContain("The engine isn't ready yet."); }); });
it('effectClear shows engine-not-ready when called before bootstrap', async () => { await withErrorContainer(async () => { vi.spyOn(BTAPI.instance, 'getRenderer').mockReturnValue(null);
BT.effectClear();
const text = document.getElementById(DEFAULT_CONTAINER_ID)?.textContent ?? ''; expect(text).toContain("The engine isn't ready yet."); }); });
it('effectAdd delegates to BTAPI.instance.effectAdd when renderer is ready', () => { vi.spyOn(BTAPI.instance, 'getRenderer').mockReturnValue({} as never); const spy = vi.spyOn(BTAPI.instance, 'effectAdd').mockReturnValue(undefined); const effect = makeStubEffect();
BT.effectAdd(effect);
expect(spy).toHaveBeenCalledWith(effect); });
it('effectRemove delegates to BTAPI.instance.effectRemove when renderer is ready', () => { vi.spyOn(BTAPI.instance, 'getRenderer').mockReturnValue({} as never); const spy = vi.spyOn(BTAPI.instance, 'effectRemove').mockReturnValue(undefined); const effect = makeStubEffect();
BT.effectRemove(effect);
expect(spy).toHaveBeenCalledWith(effect); });
it('effectClear delegates to BTAPI.instance.effectClear when renderer is ready', () => { vi.spyOn(BTAPI.instance, 'getRenderer').mockReturnValue({} as never); const spy = vi.spyOn(BTAPI.instance, 'effectClear').mockReturnValue(undefined);
BT.effectClear();
expect(spy).toHaveBeenCalled(); });
it('effectAdd shows a clear software-renderer unsupported message', async () => { await withErrorContainer(async () => { vi.spyOn(BTAPI.instance, 'getRenderer').mockReturnValue({} as never); vi.spyOn(BTAPI.instance, 'effectAdd').mockImplementation(() => { throw new Error( "The software renderer doesn't support fullscreen effects. To use post-process effects, set renderer to 'webgpu' in configure().", ); });
BT.effectAdd(makeStubEffect());
const text = document.getElementById(DEFAULT_CONTAINER_ID)?.textContent ?? ''; expect(text).toContain("doesn't support fullscreen effects"); expect(text).toContain("set renderer to 'webgpu' in configure()"); }); });
it('effectRemove shows a clear software-renderer unsupported message', async () => { await withErrorContainer(async () => { vi.spyOn(BTAPI.instance, 'getRenderer').mockReturnValue({} as never); vi.spyOn(BTAPI.instance, 'effectRemove').mockImplementation(() => { throw new Error( "The software renderer doesn't support fullscreen effects. To use post-process effects, set renderer to 'webgpu' in configure().", ); });
BT.effectRemove(makeStubEffect());
const text = document.getElementById(DEFAULT_CONTAINER_ID)?.textContent ?? ''; expect(text).toContain("doesn't support fullscreen effects"); }); });
it('effectClear shows a clear software-renderer unsupported message', async () => { await withErrorContainer(async () => { vi.spyOn(BTAPI.instance, 'getRenderer').mockReturnValue({} as never); vi.spyOn(BTAPI.instance, 'effectClear').mockImplementation(() => { throw new Error( "The software renderer doesn't support fullscreen effects. To use post-process effects, set renderer to 'webgpu' in configure().", ); });
BT.effectClear();
const text = document.getElementById(DEFAULT_CONTAINER_ID)?.textContent ?? ''; expect(text).toContain("doesn't support fullscreen effects"); }); });});
// #endregion
// #region BT.activeBackend
describe('BT.activeBackend', () => { beforeEach(() => { vi.restoreAllMocks(); });
it('delegates to BTAPI.instance.getActiveBackend', () => { const spy = vi.spyOn(BTAPI.instance, 'getActiveBackend').mockReturnValue('webgpu');
const result = BT.activeBackend;
expect(spy).toHaveBeenCalled(); expect(result).toBe('webgpu'); });
it('returns null before initialization', () => { vi.spyOn(BTAPI.instance, 'getActiveBackend').mockReturnValue(null);
expect(BT.activeBackend).toBeNull(); });
it('returns software when BTAPI reports software backend active', () => { vi.spyOn(BTAPI.instance, 'getActiveBackend').mockReturnValue('software');
expect(BT.activeBackend).toBe('software'); });});
// #endregion