import { act, render } from "@testing-library/react"; import HeroFieldScene from "./HeroFieldScene"; /** * Caminho-base de mountField que HeroFieldScene.test.tsx não exerce: o erro que vem ANTES de as peças existirem * (a nuvem de pontos ou a câmera não se criam). O renderizador já abriu o contexto WebGL, então precisa ser solto * (`disposeRenderer`, o `else` do `catch`), o canvas sai da página e a página é avisada. Os outros caminhos de * mountField (desmontada antes da 1ª tarefa, entre o renderizador e as peças, durante a compilação, shader que não * compila, sucesso) estão em HeroFieldScene.test.tsx. */ const mockRenderers: MockRenderer[] = []; let mockBuildThrows = false; class MockRenderer { domElement = document.createElement("canvas"); render = jest.fn(); compileAsync = jest.fn(() => Promise.resolve()); dispose = jest.fn(); forceContextLoss = jest.fn(); setSize = jest.fn(); setPixelRatio = jest.fn(); // um WebGL que não conta o nome do renderizador (sem WEBGL_debug_renderer_info): não é por software getContext = () => ({ getExtension: () => null }); getPixelRatio = () => 1; constructor() { mockRenderers.push(this); } } jest.mock("three", () => { const actual = jest.requireActual("three"); class FailingCamera extends actual.PerspectiveCamera { constructor(...args: unknown[]) { if (mockBuildThrows) throw new Error("sem memória para a cena"); super(...args); } } return { ...actual, PerspectiveCamera: FailingCamera, WebGLRenderer: jest.fn(() => new MockRenderer()), }; }); beforeEach(() => { jest.useFakeTimers(); mockRenderers.length = 0; mockBuildThrows = false; }); afterEach(() => { jest.useRealTimers(); }); const props = () => ({ progressRef: { current: 0 }, visible: false, onReady: jest.fn(), onError: jest.fn(), }); const buildScene = () => act(async () => { await jest.advanceTimersByTimeAsync(10); }); describe("mountField: erro antes de as peças existirem", () => { it("path: the scene parts fail to build -> the renderer opened a moment ago is released, the canvas leaves and the page is told once", async () => { mockBuildThrows = true; const scene = props(); const { container } = render(); await buildScene(); expect(mockRenderers).toHaveLength(1); expect(mockRenderers[0].compileAsync).not.toHaveBeenCalled(); expect(mockRenderers[0].dispose).toHaveBeenCalledTimes(1); expect(mockRenderers[0].forceContextLoss).toHaveBeenCalledTimes(1); expect(mockRenderers[0].domElement.isConnected).toBe(false); expect(container.querySelector("canvas")).toBeNull(); expect(scene.onError).toHaveBeenCalledTimes(1); expect(scene.onReady).not.toHaveBeenCalled(); expect(jest.getTimerCount()).toBe(0); }); it("path: nothing fails -> the same setup mounts, draws and is released only on unmount (the control for the case above)", async () => { const scene = props(); const { container, unmount } = render(); await buildScene(); expect(mockRenderers[0].compileAsync).toHaveBeenCalledTimes(1); expect(mockRenderers[0].dispose).not.toHaveBeenCalled(); expect(container.querySelector("canvas")).not.toBeNull(); expect(scene.onError).not.toHaveBeenCalled(); unmount(); expect(mockRenderers[0].dispose).toHaveBeenCalledTimes(1); }); });