257 lines
9.9 KiB
JavaScript
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);
|
|
}
|
|
}
|
|
}
|