Files
Battle-for-Tismo/tests/capabilities_test.js

257 lines
9.9 KiB
JavaScript

import { TestCase } from "./framework/test_case.js";
import { setupDom, teardownDom } from "./framework/dom.js";
import { GameScreen } from "../client/js/game_screen.js";
import { CIVILISATIONS } from "../shared/data.js";
import { smallState } from "./framework/helpers.js";
import {
NEEDED_TEXTURE_UNITS,
NEEDED_VERTEX_ATTRIBS,
capabilityStatus,
detectCapabilities,
detectWebAssembly,
formatCapabilityReport,
summarizeCapabilityReport,
} from "../client/js/capabilities.js";
import { CapabilityModal } from "../client/js/modals/capabilities.js";
// A stand-in WebGL context that answers exactly the surface `detectCapabilities`
// probes, so the checks can be exercised without a GPU.
function createMockGL(overrides = {}) {
const constants = {
VERTEX_SHADER: 0x8b31,
FRAGMENT_SHADER: 0x8b30,
COMPILE_STATUS: 0x8b81,
LINK_STATUS: 0x8b82,
MAX_TEXTURE_IMAGE_UNITS: 0x8872,
MAX_VERTEX_ATTRIBS: 0x8869,
MAX_TEXTURE_SIZE: 0x0d33,
VENDOR: 0x1f00,
RENDERER: 0x1f01,
TEXTURE_2D: 0x0de1,
RGBA: 0x1908,
UNSIGNED_BYTE: 0x1401,
FRAMEBUFFER: 0x8d40,
COLOR_ATTACHMENT0: 0x8ce0,
FRAMEBUFFER_COMPLETE: 0x8cd5,
};
const parameters = {
[constants.MAX_TEXTURE_IMAGE_UNITS]: overrides.textureUnits ?? 16,
[constants.MAX_VERTEX_ATTRIBS]: overrides.vertexAttribs ?? 16,
[constants.MAX_TEXTURE_SIZE]: overrides.maxTextureSize ?? 8192,
[constants.VENDOR]: overrides.vendor ?? "Test Vendor",
[constants.RENDERER]: overrides.renderer ?? "Test GPU",
};
const extensions = overrides.extensions || {};
const compileOk = !overrides.compileFails;
const gl = {
...constants,
getParameter: (name) => (name in parameters ? parameters[name] : null),
getExtension: (name) => extensions[name] || null,
createShader: () => ({ shader: true }),
shaderSource: () => {},
compileShader: () => {},
getShaderParameter: () => compileOk,
getShaderInfoLog: () => (compileOk ? "" : "ERROR: 0:1: sampler limit exceeded"),
deleteShader: () => {},
createProgram: () => ({ program: true }),
attachShader: () => {},
linkProgram: () => {},
getProgramParameter: () => true,
getProgramInfoLog: () => "",
deleteProgram: () => {},
createTexture: () => ({ texture: true }),
bindTexture: () => {},
texImage2D: () => {},
deleteTexture: () => {},
createFramebuffer: () => ({ fbo: true }),
bindFramebuffer: () => {},
framebufferTexture2D: () => {},
checkFramebufferStatus: () => constants.FRAMEBUFFER_COMPLETE,
deleteFramebuffer: () => {},
};
if (overrides.framebufferFails) gl.checkFramebufferStatus = () => 0x8cd6;
return gl;
}
function checkById(report, id) {
return report.checks.find((entry) => entry.id === id);
}
export class CapabilitiesTest extends TestCase {
async test_missing_context_reports_the_dom_fallback_and_requirements() {
const env = await setupDom();
try {
const report = detectCapabilities();
this.assertFalse(report.webgl.available, "WebGL is reported unavailable");
this.assertEqual(report.renderer, "dom", "the DOM renderer is the verdict");
this.assertFalse(checkById(report, "webgl-context").ok, "the context check fails");
this.assertFalse(report.ok, "a missing context is not a supported report");
this.assertNotEmpty(report.minRequirements, "the requirements are listed");
this.assertTrue(
report.reason.includes("WebGL"),
"the reason names the missing GPU path"
);
const text = formatCapabilityReport(report);
this.assertTrue(text.includes("WebAssembly"), "the report mentions WebAssembly");
this.assertTrue(text.includes("Minimum requirements"), "the report lists the minimums");
} finally {
teardownDom(env);
}
}
async test_supported_context_passes_every_required_check() {
const report = detectCapabilities({ gl: createMockGL(), version: 2 });
this.assertTrue(report.webgl.available, "the context is available");
this.assertEqual(report.webgl.name, "WebGL 2", "the version is reported");
this.assertTrue(report.ok, "all required checks pass");
this.assertEqual(report.renderer, "webgl2", "the GPU verdict is used");
for (const entry of report.checks) {
if (entry.level === "required") this.assertTrue(entry.ok, `${entry.id} should pass`);
}
}
async test_too_few_texture_units_makes_a_required_check_fail() {
const report = detectCapabilities({
gl: createMockGL({ textureUnits: NEEDED_TEXTURE_UNITS - 1 }),
version: 1,
});
const units = checkById(report, "texture-units");
this.assertFalse(units.ok, "the texture-unit check fails");
this.assertFalse(report.ok, "the report is not supported");
this.assertEqual(report.renderer, "dom", "the DOM renderer is the verdict");
this.assertEqual(capabilityStatus(units), "FAIL", "the row is marked FAIL");
}
async test_too_few_vertex_attributes_fails() {
const report = detectCapabilities({
gl: createMockGL({ vertexAttribs: NEEDED_VERTEX_ATTRIBS - 1 }),
version: 1,
});
this.assertFalse(checkById(report, "vertex-attribs").ok, "the attribute check fails");
this.assertFalse(report.ok, "the report is not supported");
}
async test_shader_compile_failure_is_reported_in_the_detail() {
const report = detectCapabilities({ gl: createMockGL({ compileFails: true }), version: 1 });
const shader = checkById(report, "terrain-program");
this.assertFalse(shader.ok, "the terrain program check fails");
this.assertTrue(shader.detail.includes("sampler limit"), "the compiler log is surfaced");
this.assertFalse(report.ok, "a broken shader blocks the GPU path");
}
async test_software_rasteriser_is_flagged_but_still_passes_required_checks() {
const report = detectCapabilities({
gl: createMockGL({ renderer: "SwiftShader Device (Subzero)" }),
version: 1,
});
this.assertTrue(report.webgl.software, "the CPU rasteriser is detected");
this.assertTrue(report.ok, "the required checks still pass");
this.assertTrue(
summarizeCapabilityReport(report).includes("software"),
"the summary explains the downgrade"
);
}
async test_optional_failures_do_not_block_the_gpu_path() {
const report = detectCapabilities({
gl: createMockGL({ extensions: {}, framebufferFails: true, compileFails: false }),
version: 1,
});
this.assertFalse(checkById(report, "framebuffer").ok, "the optional buffer check can fail");
this.assertEqual(capabilityStatus(checkById(report, "framebuffer")), "OPTIONAL", "it is optional");
this.assertTrue(report.ok, "optional gaps keep the GPU path");
}
async test_wasm_is_reported_as_informational_only() {
const wasm = detectWebAssembly();
this.assertTrue(wasm.available, "Node/browsers expose WebAssembly");
const report = detectCapabilities({ gl: createMockGL(), version: 1 });
const entry = checkById(report, "webassembly");
this.assertEqual(entry.level, "info", "WASM is informational, not required");
this.assertEqual(capabilityStatus(entry), "INFO", "and it is never marked FAIL");
}
async test_format_lists_every_check_and_requirement() {
const report = detectCapabilities({ gl: createMockGL(), version: 2 });
const text = formatCapabilityReport(report);
for (const entry of report.checks) {
this.assertTrue(text.includes(entry.label), `the report lists ${entry.id}`);
}
for (const requirement of report.minRequirements) {
this.assertTrue(text.includes(requirement), "the report lists every requirement");
}
}
async test_modal_renders_a_row_per_check_and_the_requirements() {
const env = await setupDom();
try {
const report = detectCapabilities();
const modal = new CapabilityModal();
this.assertFalse(modal.isShowing(), "the modal starts hidden");
modal.show(report);
this.assertTrue(modal.isShowing(), "the modal is shown");
this.assertEqual(
env.$("#capabilities-list .capability-row").length,
report.checks.length,
"one row per check"
);
this.assertEqual(
env.$("#capabilities-min li").length,
report.minRequirements.length,
"one line per requirement"
);
this.assertNotEmpty(env.$("#capabilities-summary").text(), "the summary is filled in");
modal.close();
this.assertFalse(modal.isShowing(), "the modal can be closed");
} finally {
teardownDom(env);
}
}
async test_joining_without_webgl_surfaces_the_report_once() {
const env = await setupDom();
const warnings = [];
const originalWarn = console.warn;
console.warn = (...args) => warnings.push(args.join(" "));
try {
const network = { sendOrder() {}, sendGameSetup() {} };
const config = {
playerName: "Carol",
playerCivId: "france",
civilisations: [CIVILISATIONS[0], CIVILISATIONS[1]],
};
const screen = new GameScreen(network, config);
screen.enter();
this.assertEqual(
screen.map.rendererFallback,
"webgl-unavailable",
"the GPU path is known to be down"
);
this.assertNull(screen.map.glRenderer, "jsdom has no GPU renderer");
const snapshot = smallState().snapshot(0);
screen.onState(snapshot);
this.assertFalse(screen.capabilityModal.isShowing(), "nothing shows before the report runs");
screen._reportCapabilities(snapshot);
this.assertTrue(screen.capabilityModal.isShowing(), "the report modal opens on join");
this.assertGreater(
env.$("#capabilities-list .capability-row").length,
0,
"it lists the features"
);
this.assertTrue(
warnings.some((line) => line.includes("capability report")),
"the report is also logged to the console"
);
screen.capabilityModal.close();
screen._reportCapabilities(snapshot);
this.assertFalse(screen.capabilityModal.isShowing(), "the report is shown at most once");
} finally {
console.warn = originalWarn;
teardownDom(env);
}
}
}