222 lines
7.3 KiB
TypeScript
222 lines
7.3 KiB
TypeScript
import {
|
|
isCoarsePointer,
|
|
isKnownSoftwareWebGL,
|
|
isLowPowerDevice,
|
|
isMobileViewport,
|
|
isSaveData,
|
|
isSoftwareRenderer,
|
|
isSoftwareWebGL,
|
|
isWebKit,
|
|
MOBILE_QUERY,
|
|
rememberSoftwareWebGL,
|
|
} 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;
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
|
|
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("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);
|
|
});
|
|
});
|