perf(movimento): vitrine e 3D mais leves, WebGL por software lembrado na sessão e suporte ao WebKit

This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 22:54:27 -03:00
1 parent 9fd1570426
commit 8903f3c57e
28 files changed
+854 -406

No files matched your search

@@ -1,10 +1,11 @@
import { act, render, screen } from "@testing-library/react";
import { rememberSoftwareWebGL } from "@/lib/device";
import HeroField, { DIVE_QUERY } from "./HeroField";
/**
* Testes de caminhos-base de HeroField (complemento de HeroField.test.tsx): como a espera pelo WebGL acaba
* (primeira interação, requestIdleCallback com folga ou sem folga, temporizador), os cancelamentos, as falhas
* (contexto WebGL2, pacote que não carrega), o fundo sem a seção `.hero`, as variáveis de rolagem e a
* (cena que não abre o contexto, pacote que não carrega), o WebGL por software lembrado na sessão, o fundo sem a seção `.hero`, as variáveis de rolagem e a
* visibilidade (observador, aba oculta, fim do mergulho) e a limpeza ao desmontar.
*/
@@ -306,25 +307,13 @@ describe("HeroField: quando o WebGL monta", () => {
});
describe("HeroField: falhas voltam ao hero simples", () => {
it("path: WebGL2 constructor exists but creating the context throws -> static background, no dive", async () => {
setContext(() => {
throw new Error("sem GPU");
});
it("path: the scene fails to open its WebGL context (it reports an error) -> static background, no dive, no scene", async () => {
render(<Hero />);
await advance(1200);
await finishLoading();
expect(section()).toHaveAttribute("data-dive");
expect(screen.queryByTestId("scene")).toBeNull();
expect(section()).not.toHaveAttribute("data-dive");
});
it("path: WebGL2 constructor exists but the context is null -> static background, no dive", async () => {
setContext(() => null);
render(<Hero />);
await advance(1200);
await finishLoading();
act(() => mockScenes.at(-1)?.onError());
expect(screen.queryByTestId("scene")).toBeNull();
expect(section()).not.toHaveAttribute("data-dive");
@@ -342,28 +331,16 @@ describe("HeroField: falhas voltam ao hero simples", () => {
expect(section()).not.toHaveAttribute("data-dive");
});
it("path: the probe context is released right away (WEBGL2 lose_context) before the real scene mounts", async () => {
const loseContext = jest.fn();
setContext(() => ({
getExtension: (name: string) =>
name === "WEBGL_lose_context" ? { loseContext } : null,
}));
render(<Hero />);
await advance(1200);
await finishLoading();
expect(loseContext).toHaveBeenCalledTimes(1);
expect(screen.getByTestId("scene")).toBeInTheDocument();
});
it("path: the lose-context extension is missing -> the probe still succeeds", async () => {
setContext(() => ({ getExtension: () => null }));
it("path: HeroField never touches a canvas context, on hydration or when idle (a test context cost a 150 to 500 ms task)", async () => {
const probe = jest.fn(() => ({ getExtension: () => null }));
setContext(probe);
render(<Hero />);
expect(probe).not.toHaveBeenCalled();
await advance(1200);
await finishLoading();
expect(probe).not.toHaveBeenCalled();
expect(screen.getByTestId("scene")).toBeInTheDocument();
});
@@ -601,61 +578,48 @@ describe("HeroField: desmontagem em pontos que faltavam", () => {
});
});
/** Um contexto WebGL de teste que conta ao navegador o nome do renderizador (WEBGL_debug_renderer_info). */
function contextRenderedBy(renderer: unknown, loseContext = jest.fn()) {
const RENDERER = 37446;
return {
getExtension: (name: string) => {
if (name === "WEBGL_debug_renderer_info")
return { UNMASKED_RENDERER_WEBGL: RENDERER };
return name === "WEBGL_lose_context" ? { loseContext } : null;
},
getParameter: (parameter: number) =>
parameter === RENDERER ? renderer : undefined,
};
}
describe("HeroField: WebGL por software (o veredito vem da cena e fica na sessão)", () => {
beforeEach(() => sessionStorage.clear());
afterEach(() => {
jest.restoreAllMocks();
sessionStorage.clear();
});
describe("HeroField: WebGL2 por software (supportsWebGL2 -> isSoftwareWebGL)", () => {
it.each([
[
"SwiftShader (Chrome sem GPU)",
"ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero)), SwiftShader driver)",
],
["llvmpipe (Mesa)", "llvmpipe (LLVM 15.0.7, 256 bits)"],
[
"Microsoft Basic Render Driver",
"ANGLE (Microsoft, Microsoft Basic Render Driver Direct3D11 vs_5_0 ps_5_0)",
],
])(
"path: %s -> the plain 100svh hero from hydration: no dive, no scroll listeners, no observer, no scene, and the probe context is still released",
async (_name, renderer) => {
const loseContext = jest.fn();
setContext(() => contextRenderedBy(renderer, loseContext));
const addListener = jest.spyOn(window, "addEventListener");
it("path: software WebGL already found in this session -> the plain 100svh hero from hydration: no dive, no scroll listeners, no observer, no scene", async () => {
rememberSoftwareWebGL();
const addListener = jest.spyOn(window, "addEventListener");
render(<Hero />);
act(() => {
window.dispatchEvent(new Event("pointermove"));
});
await advance(3000);
await finishLoading();
render(<Hero />);
act(() => {
window.dispatchEvent(new Event("pointermove"));
});
await advance(3000);
await finishLoading();
expect(section()).not.toHaveAttribute("data-dive");
expect(screen.queryByTestId("scene")).toBeNull();
expect(mockScenes).toHaveLength(0);
expect(mockPrepare).not.toHaveBeenCalled();
expect(observers).toHaveLength(0);
expect(addListener.mock.calls.map(([type]) => type)).not.toContain(
"scroll",
);
expect(loseContext).toHaveBeenCalledTimes(1);
},
);
it("path: a hardware renderer -> the scene mounts and the dive starts", async () => {
setContext(() =>
contextRenderedBy("ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11)"),
expect(section()).not.toHaveAttribute("data-dive");
expect(screen.queryByTestId("scene")).toBeNull();
expect(mockScenes).toHaveLength(0);
expect(mockPrepare).not.toHaveBeenCalled();
expect(observers).toHaveLength(0);
expect(addListener.mock.calls.map(([type]) => type)).not.toContain(
"scroll",
);
});
it("path: nothing known about this device yet -> the dive starts at hydration and the scene mounts", async () => {
render(<Hero />);
expect(section()).toHaveAttribute("data-dive");
await advance(1200);
await finishLoading();
expect(screen.getByTestId("scene")).toBeInTheDocument();
});
it("path: sessionStorage blocked (privacy mode) -> counts as unknown, the scene still mounts", async () => {
jest.spyOn(Storage.prototype, "getItem").mockImplementation(() => {
throw new Error("SecurityError");
});
render(<Hero />);
await advance(1200);
@@ -665,24 +629,16 @@ describe("HeroField: WebGL2 por software (supportsWebGL2 -> isSoftwareWebGL)", (
expect(section()).toHaveAttribute("data-dive");
});
it("path: no renderer info extension (Firefox, Safari) -> cannot tell, so the scene mounts", async () => {
setContext(() => ({ getExtension: () => null }));
it("path: the scene finds software WebGL once it has opened its context (onError) -> back to the 100svh hero, no scene", async () => {
render(<Hero />);
await advance(1200);
await finishLoading();
expect(section()).toHaveAttribute("data-dive");
expect(screen.getByTestId("scene")).toBeInTheDocument();
});
act(() => mockScenes.at(-1)?.onError());
it("path: the renderer name is not a string -> cannot tell, so the scene mounts", async () => {
setContext(() => contextRenderedBy(null));
render(<Hero />);
await advance(1200);
await finishLoading();
expect(screen.getByTestId("scene")).toBeInTheDocument();
expect(section()).not.toHaveAttribute("data-dive");
expect(screen.queryByTestId("scene")).toBeNull();
});
it.each([