import { isCoarsePointer, isLowPowerDevice, isMobileViewport, isSaveData, isSoftwareRenderer, isSoftwareWebGL, MOBILE_QUERY, } 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) { 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; }); describe("device hints", () => { it("treats a capable desktop as full power", () => { expect(isLowPowerDevice()).toBe(false); 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([ ["a touch pointer", () => mockMedia(["(pointer: coarse)"])], ["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 })], ])("is low power with %s", (_label, arrange) => { arrange(); expect(isLowPowerDevice()).toBe(true); }); it("does not penalize a browser that hides the hardware hints", () => { mockNavigator({ hardwareConcurrency: undefined, deviceMemory: undefined }); expect(isLowPowerDevice()).toBe(false); }); it("keeps 8 GB and 6 cores on the full path", () => { mockNavigator({ hardwareConcurrency: 6, deviceMemory: 8 }); expect(isLowPowerDevice()).toBe(false); }); }); 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); }); });