feat(site): blocos 3D da marca, novas ofertas e hero mais abrangente
Hero e movimento - cena 3D em tempo real com os blocos da marca (kit feito no Blender, 3d/blocos.py): marca, tela, integrações e dados; segue a rolagem, tem vida em repouso, reage ao cursor e, parada no topo, passa sozinha pelos estados com legenda - três níveis de aparelho (full, lite, static); sem WebGL fica o pôster - título novo: "Sites e sistemas sob medida para o jeito que a sua empresa trabalha." - faixa de clientes com o ramo, linha do primeiro passo, hero alinhado às seções - rótulos digitados, entrada dos blocos, cortina de blocos entre páginas e capa compartilhada entre a lista de cases e o case Ofertas - atendimento no WhatsApp com IA e nota fiscal para a reforma tributária (páginas de serviço, cartões da home, formulário e aviso com data de validade) - página /ia atualizada, com fontes - peças 3D em loop nos cartões e topos de página (3d/pecas.py) Design - hover por papel (bloco nos botões, barra nos links), rótulos sem caixa alta - um destino por rótulo: "Falar sobre meu projeto" leva ao formulário, "Chamar no WhatsApp" abre o WhatsApp - logo montado em blocos, 404 redesenhado, grades sem célula vazia - ícones do PWA e manifesto novos Specs em specs/, contexto de design em DESIGN.md e PRODUCT.md.
This commit is contained in:
1 parent
4671837c44
commit
c7d09d63d4
165 files changed
+7940
-3790
No files matched your search
@@ -0,0 +1,614 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { act, render } from "@testing-library/react";
|
||||
import type {
|
||||
InstancedBufferGeometry,
|
||||
Mesh,
|
||||
ShaderMaterial,
|
||||
Texture,
|
||||
} from "three";
|
||||
import { stateAt } from "@/lib/blocos";
|
||||
import { isKnownSlowScene, isKnownSoftwareWebGL } from "@/lib/device";
|
||||
import BlocosScene from "./BlocosScene";
|
||||
|
||||
/*
|
||||
* A cena de blocos: baixa os ativos, cria o renderizador do Three.js, compila o shader em tarefas separadas e desenha
|
||||
* sob demanda. O WebGL do jsdom não existe: o renderizador é um dublê, e os ativos (o bloco do glTF, os matcaps e o
|
||||
* JSON dos estados) vêm de dublês também; o JSON é o arquivo real.
|
||||
*/
|
||||
|
||||
type RendererOptions = {
|
||||
alpha?: boolean;
|
||||
antialias?: boolean;
|
||||
powerPreference?: string;
|
||||
};
|
||||
const mockRenderers: MockRenderer[] = [];
|
||||
/** O nome do renderizador que o contexto de teste conta (WEBGL_debug_renderer_info); `null` = a extensão não existe. */
|
||||
let mockRendererName: string | null = null;
|
||||
|
||||
class MockRenderer {
|
||||
domElement = document.createElement("canvas");
|
||||
pixelRatio = 1;
|
||||
options: RendererOptions;
|
||||
render = jest.fn();
|
||||
compileAsync = jest.fn(() => Promise.resolve());
|
||||
dispose = jest.fn();
|
||||
forceContextLoss = jest.fn();
|
||||
setSize = jest.fn();
|
||||
setPixelRatio = (value: number) => {
|
||||
this.pixelRatio = value;
|
||||
};
|
||||
getPixelRatio = () => this.pixelRatio;
|
||||
getContext = () => ({
|
||||
getExtension: (name: string) =>
|
||||
name === "WEBGL_debug_renderer_info" && mockRendererName !== null
|
||||
? { UNMASKED_RENDERER_WEBGL: 37446 }
|
||||
: null,
|
||||
getParameter: (parameter: number) =>
|
||||
parameter === 37446 ? mockRendererName : null,
|
||||
});
|
||||
constructor(options: RendererOptions) {
|
||||
this.options = options;
|
||||
mockRenderers.push(this);
|
||||
}
|
||||
}
|
||||
|
||||
jest.mock("three", () => {
|
||||
const three = jest.requireActual("three");
|
||||
return {
|
||||
...three,
|
||||
WebGLRenderer: jest.fn(
|
||||
(options: RendererOptions) => new MockRenderer(options),
|
||||
),
|
||||
// o jsdom não carrega imagens: o matcap é uma textura vazia
|
||||
TextureLoader: jest.fn(() => ({
|
||||
loadAsync: () => Promise.resolve(new three.Texture()),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock("three/examples/jsm/loaders/GLTFLoader.js", () => {
|
||||
const { BoxGeometry, Group, Mesh } = jest.requireActual("three");
|
||||
return {
|
||||
GLTFLoader: jest.fn(() => ({
|
||||
loadAsync: () => {
|
||||
const scene = new Group();
|
||||
scene.add(new Mesh(new BoxGeometry(1, 1, 1)));
|
||||
return Promise.resolve({ scene });
|
||||
},
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
const blocosJson = JSON.parse(
|
||||
readFileSync(join(process.cwd(), "public/3d/blocos.json"), "utf8"),
|
||||
);
|
||||
let mockFetchOk = true;
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
const props = () => ({
|
||||
progressRef: { current: 0 },
|
||||
visible: true,
|
||||
onReady: jest.fn(),
|
||||
onError: jest.fn(),
|
||||
});
|
||||
|
||||
function setDevicePixelRatio(value: number) {
|
||||
Object.defineProperty(window, "devicePixelRatio", {
|
||||
configurable: true,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
mockRenderers.length = 0;
|
||||
mockRendererName = null;
|
||||
mockFetchOk = true;
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: mockFetchOk,
|
||||
status: mockFetchOk ? 200 : 404,
|
||||
json: () => Promise.resolve(blocosJson),
|
||||
}),
|
||||
) as unknown as typeof fetch;
|
||||
setDevicePixelRatio(1);
|
||||
sessionStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
jest.restoreAllMocks();
|
||||
global.fetch = originalFetch;
|
||||
sessionStorage.clear();
|
||||
});
|
||||
|
||||
/** Deixa as tarefas da montagem correrem (ativos, renderizador, compilação do shader, primeiro quadro pedido). */
|
||||
const buildScene = (ms = 10) =>
|
||||
act(async () => {
|
||||
await jest.advanceTimersByTimeAsync(ms);
|
||||
});
|
||||
|
||||
async function mount(ui: React.ReactElement) {
|
||||
const result = render(ui);
|
||||
await buildScene();
|
||||
return result;
|
||||
}
|
||||
|
||||
const advance = (ms: number) =>
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(ms);
|
||||
});
|
||||
|
||||
/** A malha que o último quadro desenhou (a cena tem uma só). */
|
||||
function drawnMesh() {
|
||||
const calls = mockRenderers[0].render.mock.calls as unknown as [
|
||||
{ children: Mesh[] },
|
||||
][];
|
||||
return calls.at(-1)?.[0].children[0] as Mesh<
|
||||
InstancedBufferGeometry,
|
||||
ShaderMaterial
|
||||
>;
|
||||
}
|
||||
const uniform = (name: string) =>
|
||||
drawnMesh().material.uniforms[name].value as number;
|
||||
|
||||
// precisa ser o primeiro: os ativos ficam em cache depois do primeiro download que dá certo
|
||||
describe("BlocosScene when the assets do not download", () => {
|
||||
it("tells the page once, creates no context, and lets the next mount try again", async () => {
|
||||
mockFetchOk = false;
|
||||
const scene = props();
|
||||
|
||||
const { container, unmount } = await mount(<BlocosScene {...scene} />);
|
||||
|
||||
expect(scene.onError).toHaveBeenCalledTimes(1);
|
||||
expect(mockRenderers).toHaveLength(0);
|
||||
expect(container.querySelector("canvas")).toBeNull();
|
||||
unmount();
|
||||
|
||||
mockFetchOk = true;
|
||||
const again = props();
|
||||
await mount(<BlocosScene {...again} />);
|
||||
expect(again.onError).not.toHaveBeenCalled();
|
||||
expect(mockRenderers).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("BlocosScene", () => {
|
||||
it("creates a transparent, antialiased renderer on the high-performance GPU, and puts its canvas in the page", async () => {
|
||||
const { container } = await mount(<BlocosScene {...props()} />);
|
||||
|
||||
expect(mockRenderers).toHaveLength(1);
|
||||
expect(mockRenderers[0].options).toEqual({
|
||||
alpha: true,
|
||||
antialias: true,
|
||||
powerPreference: "high-performance",
|
||||
});
|
||||
const canvas = container.querySelector("canvas");
|
||||
expect(canvas).toBe(mockRenderers[0].domElement);
|
||||
expect(canvas).toHaveAttribute("aria-hidden", "true");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["full", 3, 1.75],
|
||||
["full", 1.5, 1.5],
|
||||
["lite", 3, 1.5],
|
||||
["lite", 0.5, 1],
|
||||
] as const)(
|
||||
"caps the pixel ratio on %s (device %p -> %p)",
|
||||
async (tier, device, expected) => {
|
||||
setDevicePixelRatio(device);
|
||||
await mount(<BlocosScene {...props()} tier={tier} />);
|
||||
expect(mockRenderers[0].pixelRatio).toBe(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it("draws every block in one instanced call that starts on marca", async () => {
|
||||
await mount(<BlocosScene {...props()} />);
|
||||
advance(20);
|
||||
|
||||
const mesh = drawnMesh();
|
||||
expect(mesh.geometry.type).toBe("InstancedBufferGeometry");
|
||||
expect(mesh.geometry.instanceCount).toBe(238);
|
||||
expect(uniform("uFrom")).toBe(0);
|
||||
expect(uniform("uTo")).toBe(0);
|
||||
});
|
||||
|
||||
it("tells the page it is ready on the second frame drawn", async () => {
|
||||
const scene = props();
|
||||
await mount(<BlocosScene {...scene} />);
|
||||
|
||||
advance(17);
|
||||
expect(mockRenderers[0].render).toHaveBeenCalledTimes(1);
|
||||
expect(scene.onReady).not.toHaveBeenCalled();
|
||||
|
||||
advance(17);
|
||||
expect(mockRenderers[0].render).toHaveBeenCalledTimes(2);
|
||||
expect(scene.onReady).toHaveBeenCalledTimes(1);
|
||||
|
||||
advance(3000);
|
||||
expect(scene.onReady).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("draws nothing while not visible", async () => {
|
||||
const frames = jest.spyOn(window, "requestAnimationFrame");
|
||||
const scene = props();
|
||||
|
||||
const { rerender } = await mount(
|
||||
<BlocosScene {...scene} visible={false} />,
|
||||
);
|
||||
advance(3000);
|
||||
expect(frames).not.toHaveBeenCalled();
|
||||
expect(jest.getTimerCount()).toBe(0);
|
||||
|
||||
rerender(<BlocosScene {...scene} visible />);
|
||||
advance(100);
|
||||
expect(mockRenderers[0].render).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("draws at screen rate through the entry and the first 60 frames, then only on the 30 fps pulse", async () => {
|
||||
const frames = jest.spyOn(window, "requestAnimationFrame");
|
||||
await mount(<BlocosScene {...props()} />);
|
||||
|
||||
advance(1500); // entrada de 1,4 s, no ritmo da tela
|
||||
const entry = frames.mock.calls.length;
|
||||
expect(entry).toBeGreaterThan(80);
|
||||
expect(uniform("uIntro")).toBe(1);
|
||||
|
||||
advance(1000);
|
||||
const pulse = frames.mock.calls.length - entry;
|
||||
expect(pulse).toBeGreaterThan(20);
|
||||
expect(pulse).toBeLessThanOrEqual(34);
|
||||
});
|
||||
|
||||
it("follows the dive progress on full", async () => {
|
||||
const scene = props();
|
||||
await mount(<BlocosScene {...scene} />);
|
||||
advance(1500);
|
||||
|
||||
scene.progressRef.current = 0.63;
|
||||
advance(4000);
|
||||
|
||||
expect(uniform("uFrom")).toBe(2);
|
||||
expect(uniform("uTo")).toBe(2);
|
||||
expect(uniform("uT")).toBe(0);
|
||||
});
|
||||
|
||||
it("changes state by itself on lite, 4 s on each, even with nobody touching the page", async () => {
|
||||
const scene = props();
|
||||
scene.progressRef.current = 0.93; // ignorado no lite
|
||||
await mount(<BlocosScene {...scene} tier="lite" />);
|
||||
|
||||
advance(1400 + 3500); // entrada e 3,5 s parado na marca
|
||||
expect([uniform("uFrom"), uniform("uTo")]).toEqual([0, 0]);
|
||||
|
||||
advance(1100); // no meio da troca de 1,2 s
|
||||
expect([uniform("uFrom"), uniform("uTo")]).toEqual([0, 1]);
|
||||
expect(uniform("uT")).toBeGreaterThan(0);
|
||||
|
||||
advance(1000);
|
||||
expect([uniform("uFrom"), uniform("uTo"), uniform("uT")]).toEqual([
|
||||
1, 1, 0,
|
||||
]);
|
||||
});
|
||||
|
||||
/** O hospedeiro com 800 x 600 px no canto da janela (o jsdom não faz layout). */
|
||||
const hostOf800x600 = () => {
|
||||
jest.spyOn(Element.prototype, "clientWidth", "get").mockReturnValue(800);
|
||||
jest.spyOn(Element.prototype, "clientHeight", "get").mockReturnValue(600);
|
||||
jest
|
||||
.spyOn(Element.prototype, "getBoundingClientRect")
|
||||
.mockReturnValue(new DOMRect(0, 0, 800, 600));
|
||||
};
|
||||
const fire = (event: Event) =>
|
||||
act(() => {
|
||||
window.dispatchEvent(event);
|
||||
});
|
||||
|
||||
it("brings the resting state to life after the entry and fades it out while the piece changes state", async () => {
|
||||
const scene = props();
|
||||
await mount(<BlocosScene {...scene} />);
|
||||
advance(1000);
|
||||
expect(uniform("uVida")).toBe(0); // a entrada ainda assenta
|
||||
|
||||
advance(1500);
|
||||
expect(uniform("uVida")).toBeGreaterThan(0.9);
|
||||
expect(uniform("uEstado")).toBe(0);
|
||||
const tempo = uniform("uTempo");
|
||||
advance(500);
|
||||
expect(uniform("uTempo")).toBeCloseTo(tempo + 0.5, 1);
|
||||
|
||||
// no meio da troca da marca para a tela
|
||||
fire(new Event("scroll"));
|
||||
scene.progressRef.current = 0.23;
|
||||
advance(1000);
|
||||
expect(uniform("uT")).toBeGreaterThan(0);
|
||||
expect(uniform("uVida")).toBeLessThan(0.05);
|
||||
|
||||
fire(new Event("scroll"));
|
||||
scene.progressRef.current = 0.35;
|
||||
advance(3000);
|
||||
expect(uniform("uEstado")).toBe(1);
|
||||
expect(uniform("uVida")).toBeGreaterThan(0.9);
|
||||
});
|
||||
|
||||
it("freezes the resting life with the pulse once nobody moves", async () => {
|
||||
const scene = props();
|
||||
scene.progressRef.current = 0.35; // fora do topo: sem o ciclo em repouso
|
||||
await mount(<BlocosScene {...scene} />);
|
||||
advance(6000); // a batida para 5 s depois do último gesto
|
||||
const drawn = mockRenderers[0].render.mock.calls.length;
|
||||
const tempo = uniform("uTempo");
|
||||
|
||||
advance(3000);
|
||||
expect(mockRenderers[0].render.mock.calls.length).toBe(drawn);
|
||||
expect(uniform("uTempo")).toBe(tempo);
|
||||
|
||||
fire(new Event("scroll"));
|
||||
advance(500);
|
||||
expect(uniform("uTempo")).toBeGreaterThan(tempo);
|
||||
});
|
||||
|
||||
/** O estado que o último quadro desenhou: de, para e t. */
|
||||
const shown = () => [uniform("uFrom"), uniform("uTo"), uniform("uT")];
|
||||
|
||||
it("parked at the top on full, cycles by itself through the four states, about 3 s on each, drawing only the changes", async () => {
|
||||
const scene = { ...props(), onEstado: jest.fn() };
|
||||
await mount(<BlocosScene {...scene} />);
|
||||
|
||||
advance(1400 + 2500); // entrada e 2,5 s parado na marca
|
||||
expect(shown()).toEqual([0, 0, 0]);
|
||||
advance(1100); // na troca de 1,2 s para a tela
|
||||
expect([uniform("uFrom"), uniform("uTo")]).toEqual([0, 1]);
|
||||
expect(uniform("uT")).toBeGreaterThan(0);
|
||||
// durante a troca, nenhuma legenda: os blocos da marca ainda ocupam o lugar dela
|
||||
expect(scene.onEstado).toHaveBeenLastCalledWith(null);
|
||||
advance(1500);
|
||||
expect(shown()).toEqual([1, 1, 0]);
|
||||
expect(scene.onEstado).toHaveBeenLastCalledWith(1); // assentada na tela
|
||||
|
||||
// a batida parou (5 s sem gesto): parada na tela, nenhum quadro até a próxima troca
|
||||
const drawn = mockRenderers[0].render.mock.calls.length;
|
||||
advance(1000);
|
||||
expect(mockRenderers[0].render.mock.calls.length).toBe(drawn);
|
||||
|
||||
advance(4200 * 2 + 1000); // integrações, dados
|
||||
expect(shown()).toEqual([3, 3, 0]);
|
||||
advance(4200);
|
||||
expect(shown()).toEqual([0, 0, 0]);
|
||||
// cada legenda entra com a peça assentada e sai quando a troca seguinte começa
|
||||
expect(scene.onEstado.mock.calls.map(([estado]) => estado)).toEqual([
|
||||
0,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
2,
|
||||
null,
|
||||
3,
|
||||
null,
|
||||
0,
|
||||
]);
|
||||
});
|
||||
|
||||
it("hands the piece back to the scroll at once and without a jump, and starts the cycle again only after 3 s at the top", async () => {
|
||||
const scene = { ...props(), onEstado: jest.fn() };
|
||||
await mount(<BlocosScene {...scene} />);
|
||||
advance(1400 + 3000 + 1200 + 1000);
|
||||
expect(shown()).toEqual([1, 1, 0]); // parada na tela, pelo ciclo
|
||||
|
||||
fire(new Event("scroll"));
|
||||
scene.progressRef.current = 0.02;
|
||||
advance(50); // a próxima batida
|
||||
// a rolagem pede a marca: a peça sai da tela para ela, do ponto em que estava
|
||||
expect([uniform("uFrom"), uniform("uTo")]).toEqual([1, 0]);
|
||||
expect(uniform("uT")).toBeGreaterThan(0);
|
||||
expect(uniform("uT")).toBeLessThan(0.1);
|
||||
expect(scene.onEstado).toHaveBeenLastCalledWith(null); // em troca: sem legenda
|
||||
advance(1000);
|
||||
expect(shown()).toEqual([0, 0, 0]);
|
||||
|
||||
// de volta ao topo: 3 s parada na marca antes da troca seguinte
|
||||
fire(new Event("scroll"));
|
||||
scene.progressRef.current = 0;
|
||||
advance(3000);
|
||||
expect(shown()).toEqual([0, 0, 0]);
|
||||
advance(1500);
|
||||
expect([uniform("uFrom"), uniform("uTo")]).toEqual([0, 1]);
|
||||
});
|
||||
|
||||
it("follows the dive once the scroll leaves the top, and tells the legend the piece has left its box", async () => {
|
||||
const scene = { ...props(), onEstado: jest.fn() };
|
||||
await mount(<BlocosScene {...scene} />);
|
||||
advance(1400 + 3000 + 1200 + 1000);
|
||||
expect(scene.onEstado).toHaveBeenLastCalledWith(1);
|
||||
|
||||
fire(new Event("scroll"));
|
||||
scene.progressRef.current = 0.63;
|
||||
advance(4000);
|
||||
expect(shown()).toEqual([2, 2, 0]);
|
||||
expect(scene.onEstado).toHaveBeenLastCalledWith(null);
|
||||
});
|
||||
|
||||
it("takes the piece from the cycle into the middle of a dive transition and keeps it there", async () => {
|
||||
const scene = props();
|
||||
await mount(<BlocosScene {...scene} />);
|
||||
advance(1400 + 4200 + 4200 + 1000);
|
||||
expect(shown()).toEqual([2, 2, 0]); // parada nas integrações, pelo ciclo
|
||||
|
||||
// a rolagem para no meio da troca da marca para a tela
|
||||
fire(new Event("scroll"));
|
||||
scene.progressRef.current = 0.26;
|
||||
advance(3000);
|
||||
expect([uniform("uFrom"), uniform("uTo")]).toEqual([0, 1]);
|
||||
expect(uniform("uT")).toBeCloseTo(stateAt(0.26).t, 6);
|
||||
});
|
||||
|
||||
it("lite: tells the legend every state it builds", async () => {
|
||||
const scene = { ...props(), onEstado: jest.fn() };
|
||||
await mount(<BlocosScene {...scene} tier="lite" />);
|
||||
advance(1400 + 4000 + 1200 + 500);
|
||||
expect(scene.onEstado.mock.calls.map(([estado]) => estado)).toEqual([
|
||||
0,
|
||||
null,
|
||||
1,
|
||||
]);
|
||||
});
|
||||
|
||||
it("pushes the blocks under the mouse forward on full and lets them go when the mouse leaves", async () => {
|
||||
hostOf800x600();
|
||||
await mount(<BlocosScene {...props()} />);
|
||||
advance(2500);
|
||||
expect(uniform("uCursorPeso")).toBe(0);
|
||||
|
||||
// o centro do hospedeiro cai no centro da peça
|
||||
fire(new PointerEvent("pointermove", { clientX: 400, clientY: 300 }));
|
||||
advance(1000);
|
||||
expect(uniform("uCursorPeso")).toBeGreaterThan(0.9);
|
||||
const cursor = drawnMesh().material.uniforms.uCursor.value as {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
expect(Math.hypot(cursor.x, cursor.y)).toBeLessThan(0.5);
|
||||
|
||||
// perto do canto de cima à esquerda, longe da peça, o ponto vai junto
|
||||
fire(new PointerEvent("pointermove", { clientX: 100, clientY: 100 }));
|
||||
advance(1000);
|
||||
expect(cursor.x).toBeLessThan(-3);
|
||||
expect(cursor.y).toBeGreaterThan(2);
|
||||
|
||||
fire(new PointerEvent("pointerout", { relatedTarget: null }));
|
||||
advance(1500);
|
||||
expect(uniform("uCursorPeso")).toBeLessThan(0.01);
|
||||
});
|
||||
|
||||
it("gives lite the resting life on the pulse, without the cursor", async () => {
|
||||
hostOf800x600();
|
||||
await mount(<BlocosScene {...props()} tier="lite" />);
|
||||
advance(2500);
|
||||
expect(uniform("uVida")).toBeGreaterThan(0.9);
|
||||
const tempo = uniform("uTempo");
|
||||
|
||||
fire(new PointerEvent("pointermove", { clientX: 400, clientY: 300 }));
|
||||
advance(500);
|
||||
expect(uniform("uTempo")).toBeCloseTo(tempo + 0.5, 1);
|
||||
expect(uniform("uCursorPeso")).toBe(0);
|
||||
});
|
||||
|
||||
it("releases the geometry, the material, the textures, the context, the canvas, the timers and the listeners on unmount", async () => {
|
||||
const removed = jest.spyOn(window, "removeEventListener");
|
||||
const scene = props();
|
||||
const { container, unmount } = await mount(<BlocosScene {...scene} />);
|
||||
advance(20);
|
||||
const mesh = drawnMesh();
|
||||
const disposed = [
|
||||
jest.spyOn(mesh.geometry, "dispose"),
|
||||
jest.spyOn(mesh.material, "dispose"),
|
||||
jest.spyOn(mesh.material.uniforms.uPorcelana.value as Texture, "dispose"),
|
||||
jest.spyOn(mesh.material.uniforms.uLima.value as Texture, "dispose"),
|
||||
];
|
||||
const canvasRemoved = jest.spyOn(
|
||||
mockRenderers[0].domElement,
|
||||
"removeEventListener",
|
||||
);
|
||||
|
||||
unmount();
|
||||
|
||||
for (const dispose of disposed) expect(dispose).toHaveBeenCalledTimes(1);
|
||||
expect(mockRenderers[0].dispose).toHaveBeenCalledTimes(1);
|
||||
expect(mockRenderers[0].forceContextLoss).toHaveBeenCalledTimes(1);
|
||||
expect(container.querySelector("canvas")).toBeNull();
|
||||
expect(jest.getTimerCount()).toBe(0);
|
||||
expect(removed.mock.calls.map(([type]) => type)).toEqual(
|
||||
expect.arrayContaining(["resize", "pointermove", "pointerout"]),
|
||||
);
|
||||
expect(canvasRemoved).toHaveBeenCalledWith(
|
||||
"webglcontextlost",
|
||||
expect.any(Function),
|
||||
);
|
||||
const drawn = mockRenderers[0].render.mock.calls.length;
|
||||
advance(1000);
|
||||
expect(mockRenderers[0].render.mock.calls.length).toBe(drawn);
|
||||
});
|
||||
|
||||
it("tells the page once when the WebGL context is lost", async () => {
|
||||
const scene = props();
|
||||
await mount(<BlocosScene {...scene} />);
|
||||
|
||||
act(() => {
|
||||
mockRenderers[0].domElement.dispatchEvent(
|
||||
new Event("webglcontextlost", { cancelable: true }),
|
||||
);
|
||||
mockRenderers[0].domElement.dispatchEvent(
|
||||
new Event("webglcontextlost", { cancelable: true }),
|
||||
);
|
||||
});
|
||||
|
||||
expect(scene.onError).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("gives up once and remembers it when the entry frames average more than 28 ms", async () => {
|
||||
// quadros de 40 ms: um aparelho que não segura a cena
|
||||
jest
|
||||
.spyOn(window, "requestAnimationFrame")
|
||||
.mockImplementation(
|
||||
(callback) =>
|
||||
setTimeout(
|
||||
() => callback(performance.now()),
|
||||
40,
|
||||
) as unknown as number,
|
||||
);
|
||||
jest
|
||||
.spyOn(window, "cancelAnimationFrame")
|
||||
.mockImplementation((id) => clearTimeout(id));
|
||||
const scene = props();
|
||||
await mount(<BlocosScene {...scene} />);
|
||||
|
||||
advance(3000);
|
||||
|
||||
expect(scene.onError).toHaveBeenCalledTimes(1);
|
||||
expect(isKnownSlowScene()).toBe(true);
|
||||
const drawn = mockRenderers[0].render.mock.calls.length;
|
||||
expect(drawn).toBeLessThanOrEqual(62);
|
||||
advance(2000);
|
||||
expect(mockRenderers[0].render.mock.calls.length).toBe(drawn);
|
||||
});
|
||||
|
||||
it("keeps a device that holds 60 fps, without remembering anything", async () => {
|
||||
const scene = props();
|
||||
await mount(<BlocosScene {...scene} />);
|
||||
|
||||
advance(3000);
|
||||
|
||||
expect(scene.onError).not.toHaveBeenCalled();
|
||||
expect(isKnownSlowScene()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("BlocosScene: software WebGL", () => {
|
||||
it("draws on the CPU: no canvas, the page is told once, the context is freed and the session remembers it", async () => {
|
||||
mockRendererName = "llvmpipe (LLVM 15.0.7, 256 bits)";
|
||||
const scene = props();
|
||||
|
||||
const { container } = await mount(<BlocosScene {...scene} />);
|
||||
|
||||
expect(mockRenderers).toHaveLength(1);
|
||||
expect(mockRenderers[0].dispose).toHaveBeenCalledTimes(1);
|
||||
expect(mockRenderers[0].forceContextLoss).toHaveBeenCalledTimes(1);
|
||||
expect(mockRenderers[0].compileAsync).not.toHaveBeenCalled();
|
||||
expect(container.querySelector("canvas")).toBeNull();
|
||||
expect(scene.onError).toHaveBeenCalledTimes(1);
|
||||
expect(scene.onReady).not.toHaveBeenCalled();
|
||||
expect(isKnownSoftwareWebGL()).toBe(true);
|
||||
});
|
||||
|
||||
it("draws on the GPU: the canvas stays and the session is not marked", async () => {
|
||||
mockRendererName = "ANGLE (NVIDIA, NVIDIA GeForce RTX 3070 Direct3D11)";
|
||||
const scene = props();
|
||||
|
||||
const { container } = await mount(<BlocosScene {...scene} />);
|
||||
|
||||
expect(container.querySelector("canvas")).toBe(mockRenderers[0].domElement);
|
||||
expect(scene.onError).not.toHaveBeenCalled();
|
||||
expect(isKnownSoftwareWebGL()).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user