Files
portifolio-evandrodevbr/lib/device.test.ts
evandro c7d09d63d4
CI / audit (push) Successful in 10m38s
CI / verify (push) Successful in 13m10s
CI / deploy (push) Successful in 1m50s
CI / e2e (push) Successful in 18m48s
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.
2026-11-10 03:48:25 -03:00

315 lines
10 KiB
TypeScript

import {
isCoarsePointer,
isKnownSlowScene,
isKnownSoftwareWebGL,
isMobileViewport,
isSaveData,
isSoftwareRenderer,
isSoftwareWebGL,
isWebKit,
MOBILE_QUERY,
rememberSlowScene,
rememberSoftwareWebGL,
sceneTier,
} from "./device";
const originalMatchMedia = window.matchMedia;
function mockMedia(active: string[]) {
window.matchMedia = jest.fn().mockImplementation((query: string) => ({
matches: active.includes(query),
media: query,
addEventListener() {},
removeEventListener() {},
}));
}
function mockNavigator(values: Record<string, unknown>) {
for (const [key, value] of Object.entries(values))
Object.defineProperty(navigator, key, { configurable: true, value });
}
beforeEach(() => {
mockMedia([]);
mockNavigator({
hardwareConcurrency: 8,
deviceMemory: undefined,
connection: undefined,
});
});
afterEach(() => {
window.matchMedia = originalMatchMedia;
});
const TOUCH = "(pointer: coarse)";
describe("device hints", () => {
it("treats a capable desktop as full power", () => {
expect(sceneTier()).toBe("full");
expect(isCoarsePointer()).toBe(false);
expect(isSaveData()).toBe(false);
expect(isMobileViewport()).toBe(false);
});
it("reads the phone breakpoint from the shared query", () => {
mockMedia([MOBILE_QUERY]);
expect(isMobileViewport()).toBe(true);
expect(MOBILE_QUERY).toBe("(max-width: 767px)");
});
it.each([
["reduced motion", () => mockMedia(["(prefers-reduced-motion: reduce)"])],
["the data saver", () => mockNavigator({ connection: { saveData: true } })],
["four cores", () => mockNavigator({ hardwareConcurrency: 4 })],
["two cores", () => mockNavigator({ hardwareConcurrency: 2 })],
["4 GB of memory", () => mockNavigator({ deviceMemory: 4 })],
["2 GB of memory", () => mockNavigator({ deviceMemory: 2 })],
])("shows only the poster with %s", (_label, arrange) => {
arrange();
expect(sceneTier()).toBe("static");
});
it("does not penalize a browser that hides the hardware hints", () => {
mockNavigator({ hardwareConcurrency: undefined, deviceMemory: undefined });
expect(sceneTier()).toBe("full");
});
it("keeps 8 GB and 6 cores on the full path", () => {
mockNavigator({ hardwareConcurrency: 6, deviceMemory: 8 });
expect(sceneTier()).toBe("full");
});
});
describe("sceneTier(): one case per row of the decision table", () => {
it("row 1: no navigator (server) -> static", () => {
const original = Object.getOwnPropertyDescriptor(globalThis, "navigator");
Object.defineProperty(globalThis, "navigator", {
configurable: true,
value: undefined,
});
try {
expect(sceneTier()).toBe("static");
} finally {
if (original) Object.defineProperty(globalThis, "navigator", original);
else Reflect.deleteProperty(globalThis, "navigator");
}
});
it.each([
["reduced motion", [TOUCH, "(prefers-reduced-motion: reduce)"], {}],
["the data saver", [TOUCH], { connection: { saveData: true } }],
])("row 2: a capable phone with %s -> static", (_label, media, hints) => {
mockMedia(media);
mockNavigator({ hardwareConcurrency: 8, deviceMemory: 8, ...hints });
expect(sceneTier()).toBe("static");
});
it("row 3: 4 cores with plenty of memory -> static even with a mouse", () => {
mockNavigator({ hardwareConcurrency: 4, deviceMemory: 8 });
expect(sceneTier()).toBe("static");
});
it.each([
["8 cores and 4 GB", { hardwareConcurrency: 8, deviceMemory: 4 }, "lite"],
["8 cores and 2 GB", { hardwareConcurrency: 8, deviceMemory: 2 }, "static"],
["5 cores", { hardwareConcurrency: 5 }, "static"],
["6 cores and 4 GB", { hardwareConcurrency: 6, deviceMemory: 4 }, "lite"],
[
"no hints reported",
{ hardwareConcurrency: undefined, deviceMemory: undefined },
"lite",
],
])("row 4: touch with %s -> %s", (_label, hints, tier) => {
mockMedia([TOUCH]);
mockNavigator(hints);
expect(sceneTier()).toBe(tier);
});
it.each([
["8 cores and 4 GB", { hardwareConcurrency: 8, deviceMemory: 4 }, "static"],
["8 cores and 8 GB", { hardwareConcurrency: 8, deviceMemory: 8 }, "full"],
["5 cores and no memory hint", { hardwareConcurrency: 5 }, "full"],
])("row 5: mouse with %s -> %s", (_label, hints, tier) => {
mockNavigator(hints);
expect(sceneTier()).toBe(tier);
});
});
describe("software WebGL", () => {
it.each([
"ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), SwiftShader driver)",
"llvmpipe (LLVM 15.0.7, 256 bits)",
"ANGLE (Microsoft, Microsoft Basic Render Driver (0x0000008C) Direct3D11 vs_5_0 ps_5_0, D3D11)",
"SWIFTSHADER",
])("recognizes the software renderer %s", (name) => {
expect(isSoftwareRenderer(name)).toBe(true);
});
it.each([
"ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 (0x00002504) Direct3D11 vs_5_0 ps_5_0, D3D11)",
"ANGLE (Apple, ANGLE Metal Renderer: Apple M2, Unspecified Version)",
"Mesa Intel(R) UHD Graphics 620 (KBL GT2)",
"",
])("does not mistake the GPU %s for software", (name) => {
expect(isSoftwareRenderer(name)).toBe(false);
});
/** Um contexto WebGL de mentira: a extensão de depuração e o nome do renderizador. */
function context(renderer: unknown, hasExtension = true) {
return {
getExtension: jest.fn((name: string) =>
hasExtension && name === "WEBGL_debug_renderer_info"
? { UNMASKED_RENDERER_WEBGL: 37446 }
: null,
),
getParameter: jest.fn((parameter: number) =>
parameter === 37446 ? renderer : null,
),
} as unknown as WebGL2RenderingContext;
}
it("reads the unmasked renderer through WEBGL_debug_renderer_info", () => {
expect(isSoftwareWebGL(context("llvmpipe (LLVM 15.0.7, 256 bits)"))).toBe(
true,
);
expect(isSoftwareWebGL(context("ANGLE (NVIDIA GeForce RTX 3060)"))).toBe(
false,
);
});
it("does not count against a browser that hides the extension or returns no name", () => {
expect(isSoftwareWebGL(context("llvmpipe", false))).toBe(false);
expect(isSoftwareWebGL(context(null))).toBe(false);
});
});
describe("the software WebGL verdict kept for the session", () => {
beforeEach(() => sessionStorage.clear());
afterEach(() => {
jest.restoreAllMocks();
sessionStorage.clear();
});
it("is unknown until someone remembers it, then known for the rest of the session", () => {
expect(isKnownSoftwareWebGL()).toBe(false);
rememberSoftwareWebGL();
expect(isKnownSoftwareWebGL()).toBe(true);
// fica no sessionStorage: some com a aba, e não vale para outra aparelhagem
expect(sessionStorage.getItem("webgl-software")).toBe("1");
});
it("ignores any other value in the key (only the verdict this module writes counts)", () => {
sessionStorage.setItem("webgl-software", "0");
expect(isKnownSoftwareWebGL()).toBe(false);
});
it("counts a blocked sessionStorage as unknown, and remembering then does not throw", () => {
jest.spyOn(Storage.prototype, "getItem").mockImplementation(() => {
throw new DOMException("blocked", "SecurityError");
});
jest.spyOn(Storage.prototype, "setItem").mockImplementation(() => {
throw new DOMException("blocked", "SecurityError");
});
expect(isKnownSoftwareWebGL()).toBe(false);
expect(() => rememberSoftwareWebGL()).not.toThrow();
});
});
describe("the slow scene verdict kept for the session", () => {
beforeEach(() => sessionStorage.clear());
afterEach(() => {
jest.restoreAllMocks();
sessionStorage.clear();
});
it("is unknown until someone remembers it, then known for the rest of the session", () => {
expect(isKnownSlowScene()).toBe(false);
rememberSlowScene();
expect(isKnownSlowScene()).toBe(true);
expect(sessionStorage.getItem("scene-slow")).toBe("1");
});
it("ignores any other value in the key (only the verdict this module writes counts)", () => {
sessionStorage.setItem("scene-slow", "0");
expect(isKnownSlowScene()).toBe(false);
});
it("counts a blocked sessionStorage as unknown, and remembering then does not throw", () => {
jest.spyOn(Storage.prototype, "getItem").mockImplementation(() => {
throw new DOMException("blocked", "SecurityError");
});
jest.spyOn(Storage.prototype, "setItem").mockImplementation(() => {
throw new DOMException("blocked", "SecurityError");
});
expect(isKnownSlowScene()).toBe(false);
expect(() => rememberSlowScene()).not.toThrow();
});
});
describe("isWebKit (the only engine that needs the showcase video to play once)", () => {
const iphone =
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1";
const iphoneChrome =
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/126.0 Mobile/15E148 Safari/604.1";
const macSafari =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15";
const chrome =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36";
const edge = `${chrome} Edg/126.0.0.0`;
const androidChrome =
"Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36";
const firefox =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:127.0) Gecko/20100101 Firefox/127.0";
afterEach(() => {
mockNavigator({ platform: "", maxTouchPoints: 0 });
Reflect.deleteProperty(navigator, "userAgent");
});
it.each([
["Safari on the iPhone", iphone],
[
"Chrome on the iPhone (iOS runs WebKit under every browser)",
iphoneChrome,
],
["Safari on the Mac", macSafari],
])("is true for %s", (_name, userAgent) => {
mockNavigator({ userAgent });
expect(isWebKit()).toBe(true);
});
it.each([
["Chrome", chrome],
["Edge", edge],
["Chrome on Android", androidChrome],
["Firefox", firefox],
])("is false for %s", (_name, userAgent) => {
mockNavigator({ userAgent });
expect(isWebKit()).toBe(false);
});
it("is true for an iPad that presents itself as a Mac (a Mac with a touch screen)", () => {
mockNavigator({
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Gecko/20100101",
platform: "MacIntel",
maxTouchPoints: 5,
});
expect(isWebKit()).toBe(true);
mockNavigator({ maxTouchPoints: 0 });
expect(isWebKit()).toBe(false);
});
});