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); } } }