diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 12eebee..e2b5845 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -29,3 +29,13 @@ jobs:
- name: Run unit tests
run: uv run python -m unittest discover -s tests
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "22"
+
+ - name: Run dashboard tests
+ run: |
+ node --check docs/js/dashboard.js
+ node --test tests/dashboard.test.mjs
diff --git a/docs/index.html b/docs/index.html
index fddd15a..5c0afa2 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -9,7 +9,7 @@
Isaac Lab Presets Performance Dashboard
-
+
diff --git a/docs/js/dashboard.js b/docs/js/dashboard.js
index 835bdf7..5a010c5 100644
--- a/docs/js/dashboard.js
+++ b/docs/js/dashboard.js
@@ -4,6 +4,13 @@
const $ = (s) => document.querySelector(s);
const $$ = (s) => [...document.querySelectorAll(s)];
+ function textElement(tag, className, value) {
+ const element = document.createElement(tag);
+ element.className = className;
+ element.textContent = String(value);
+ return element;
+ }
+
const PRESET_COLORS = [
"#00d4ff", "#ff3e96", "#00ff88", "#ff6b35", "#ffea00",
"#7c4dff", "#00bcd4", "#e040fb", "#69f0ae", "#ff5722",
@@ -677,7 +684,10 @@
if (pendingMemCharts.length > 0) {
const memHeading = document.createElement("div");
memHeading.className = "scaling-category-heading";
- memHeading.innerHTML = `GPU Memory Usage${gpuTotalMemGb ? `Total VRAM: ${formatNumber(gpuTotalMemGb)} GB` : ""}`;
+ memHeading.append(
+ textElement("span", "", "GPU Memory Usage"),
+ textElement("span", "scaling-category-sub", gpuTotalMemGb ? `Total VRAM: ${formatNumber(gpuTotalMemGb)} GB` : ""),
+ );
container.appendChild(memHeading);
for (const { rawTask, wfShort, memData: md, memPresetLabels: mpl } of pendingMemCharts) {
@@ -805,7 +815,9 @@
for (const item of items) {
const div = document.createElement("div");
div.className = "info-env-item";
- div.innerHTML = `${item.label}${item.value}`;
+ const value = textElement("span", "value", item.value);
+ value.title = String(item.value);
+ div.append(textElement("span", "label", item.label), value);
container.appendChild(div);
}
}
@@ -927,9 +939,7 @@
if (warnings.length > 0) {
const banner = document.createElement("div");
banner.className = "warning-banner";
- banner.innerHTML = warnings
- .map((w) => `\u26A0 ${w}
`)
- .join("");
+ banner.append(...warnings);
container.appendChild(banner);
}
@@ -1080,6 +1090,16 @@
function detectPresetMismatches(run) {
const warnings = [];
+ function addWarning(preset, kind, configured, actual) {
+ const line = document.createElement("div");
+ line.className = "warning-line";
+ line.append(
+ "\u26A0 Preset ", textElement("strong", "", preset),
+ ` configured ${kind} as `, textElement("strong", "", configured),
+ ", but ", textElement("strong", "", actual), " was actually used.",
+ );
+ warnings.push(line);
+ }
for (const entry of run.entries) {
if (!entry.preset || isHiddenPreset(entry.preset)) continue;
const p = parsePreset(entry.preset);
@@ -1090,14 +1110,10 @@
const presetPhys = p.physics.toLowerCase();
if (actualPhys && presetPhys && actualPhys !== presetPhys) {
- warnings.push(
- `Preset ${entry.preset} configured physics as ${p.physics}, but ${entry.actual_physics} was actually used.`
- );
+ addWarning(entry.preset, "physics", p.physics, entry.actual_physics);
}
if (actualRend && p.renderer && !rendererMatches(p.renderer, actualRend)) {
- warnings.push(
- `Preset ${entry.preset} configured renderer as ${p.renderer}, but ${actualRend} was actually used.`
- );
+ addWarning(entry.preset, "renderer", p.renderer, actualRend);
}
}
return warnings;
@@ -1178,15 +1194,21 @@
const mi = presetMaxIter[item.preset];
const cmd = buildShellCmd(workflow, rawTask, numEnvs, rawPreset, nf, mi);
const vram = vramValues[item.preset];
- let vramStr = "";
+ const rankLabel = textElement("span", "rank-label", label);
+ if (numEnvs) {
+ rankLabel.append(" ", textElement("span", "rank-envs", `(${numEnvs} envs)`));
+ }
if (vram != null) {
const pct = gpuTotalGb ? ((vram / gpuTotalGb) * 100).toFixed(1) : null;
- vramStr = pct
- ? ` ${formatNumber(vram)} GB / ${pct}% VRAM`
- : ` ${formatNumber(vram)} GB VRAM`;
+ const vramText = pct
+ ? `${formatNumber(vram)} GB / ${pct}% VRAM`
+ : `${formatNumber(vram)} GB VRAM`;
+ rankLabel.append(" ", textElement("span", "rank-vram", vramText));
}
- li.innerHTML = `${label}${numEnvs ? ` (${numEnvs} envs)` : ""}${vramStr}${formatNumber(item.value)}`;
- li.querySelector(".copy-cmd-btn").addEventListener("click", (e) => {
+ const copyButton = textElement("button", "copy-cmd-btn", "\uD83D\uDCCB");
+ copyButton.title = "Copy run command";
+ li.append(copyButton, rankLabel, textElement("span", "rank-value", formatNumber(item.value)));
+ copyButton.addEventListener("click", (e) => {
e.stopPropagation();
const btn = e.currentTarget;
function onSuccess() {
@@ -1347,11 +1369,14 @@
for (const preset of currentOrder) {
const th = document.createElement("th");
const p = parsePreset(preset);
- th.innerHTML = ``;
+ const header = document.createElement("div");
+ header.className = "preset-header-cell";
+ header.append(
+ textElement("span", "preset-part physics", p.physics),
+ textElement("span", "preset-part renderer", p.renderer),
+ textElement("span", "preset-part datatype", p.datatype),
+ );
+ th.appendChild(header);
headerRow.appendChild(th);
}
thead.appendChild(headerRow);
@@ -1436,7 +1461,9 @@
for (const item of items) {
const div = document.createElement("div");
div.className = "version-item";
- div.innerHTML = `${item.label}${item.value}`;
+ const value = textElement("span", "version-value", item.value);
+ value.title = String(item.value);
+ div.append(textElement("span", "version-label", item.label), value);
bar.appendChild(div);
}
}
@@ -1805,10 +1832,10 @@
const p = parsePreset(entry.preset);
const div = document.createElement("div");
div.className = "summary-metric";
- div.innerHTML = `
- ${p.physics} / ${p.renderer} / ${p.datatype}
- ${formatNumber(val)}
- `;
+ div.append(
+ textElement("div", "label", `${p.physics} / ${p.renderer} / ${p.datatype}`),
+ textElement("div", "value", formatNumber(val)),
+ );
content.appendChild(div);
}
}
@@ -2075,7 +2102,10 @@
const info = allWfTasks[col];
const th = document.createElement("th");
const wfShort = info.workflow.replace("benchmark_", "").replace("_train", "");
- th.innerHTML = ``;
+ const header = document.createElement("div");
+ header.className = "status-col-header";
+ header.append(textElement("span", "", wfShort), textElement("span", "status-task-name", info.task));
+ th.appendChild(header);
hRow.appendChild(th);
}
thead.appendChild(hRow);
diff --git a/tests/dashboard.test.mjs b/tests/dashboard.test.mjs
new file mode 100644
index 0000000..987f0c9
--- /dev/null
+++ b/tests/dashboard.test.mjs
@@ -0,0 +1,147 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+import assert from 'node:assert/strict';
+import { readFileSync } from 'node:fs';
+import { test } from 'node:test';
+import vm from 'node:vm';
+
+// A DOM recorder for the synchronous renderers. It deliberately does not parse
+// HTML: benchmark values must reach text nodes, with presentation nodes retained.
+function loadDashboard() {
+ const htmlWrites = [];
+ class Element {
+ constructor(tagName) {
+ this.tagName = tagName;
+ this.children = [];
+ this.className = '';
+ this.style = {};
+ this.listeners = {};
+ this.classList = {
+ add: (...names) => { this.className += ' ' + names.join(' '); },
+ toggle: () => {},
+ };
+ }
+ set textContent(value) { this.children = []; this.text = String(value); }
+ get textContent() { return (this.text ?? '') + this.children.map(child => child.textContent).join(''); }
+ set innerHTML(value) { this.children = []; this.text = ''; this.html = value; htmlWrites.push(value); }
+ get innerHTML() { return this.html ?? ''; }
+ appendChild(child) { this.children.push(child); return child; }
+ append(...children) {
+ for (const child of children) {
+ if (typeof child === 'string') {
+ const text = new Element('#text');
+ text.textContent = child;
+ this.appendChild(text);
+ } else this.appendChild(child);
+ }
+ }
+ addEventListener(name, fn) { this.listeners[name] = fn; }
+ querySelectorAll(selector) {
+ const matches = node => selector.startsWith('.')
+ ? node.className.split(/\s+/).includes(selector.slice(1))
+ : node.tagName === selector;
+ return this.children.flatMap(child => [
+ ...(matches(child) ? [child] : []), ...child.querySelectorAll(selector),
+ ]);
+ }
+ querySelector(selector) {
+ return this.querySelectorAll(selector)[0] ?? null;
+ }
+ }
+ const roots = new Map();
+ const document = {
+ createElement: tag => new Element(tag),
+ createTextNode: text => { const node = new Element('#text'); node.textContent = text; return node; },
+ querySelector: selector => {
+ if (!roots.has(selector)) roots.set(selector, new Element('div'));
+ return roots.get(selector);
+ },
+ };
+ const context = vm.createContext({ document, URLSearchParams,
+ window: { location: { pathname: '/' } }, history: { replaceState() {} },
+ });
+ const source = readFileSync(new URL('../docs/js/dashboard.js', import.meta.url), 'utf8');
+ const marker = ' /* ─── Init & Events ─── */';
+ assert.ok(source.includes(marker));
+ vm.runInContext(source.slice(0, source.indexOf(marker)) + `
+ globalThis.dashboard = { state, renderInfo, renderVersionBar, detectPresetMismatches,
+ buildFullTable, buildFpsRankCard, showHistRunDetail, renderBenchmarkStatusGrid };
+ })();`, context);
+ return { ...context.dashboard, document, htmlWrites };
+}
+
+const label = 'Team A & B "release"';
+const preset = `${label},Renderer,RGB`;
+const run = {
+ commit_sha: 'abc123', commit_date: '2026-01-01', isaac_sim_version: label,
+ hostname: label, env_info: {}, entries: [{ preset, ci_job_id: '123',
+ actual_physics: 'Physics & Simulation', benchmarks: [{
+ workflow: 'benchmark_non_rl', task: label, runtime: { FPS: 12 },
+ }],
+ }],
+};
+
+test('environment values retain their literal text and titles', () => {
+ const d = loadDashboard();
+ d.state.currentGpu = 'gpu';
+ d.state.manifest = { datasets: [{ gpu_type: 'gpu', file: 'gpu' }] };
+ d.state.datasets.gpu = { runs: [run] };
+ d.renderInfo();
+ const values = d.document.querySelector('#infoBenchmarkEnv').querySelectorAll('.value');
+ assert.ok(values.some(value => value.textContent === label && value.title === label));
+ assert.ok(!d.htmlWrites.some(html => html.includes(label)));
+});
+
+test('version values retain their literal text and titles', () => {
+ const d = loadDashboard();
+ d.renderVersionBar(run, '#versions');
+ const values = d.document.querySelector('#versions').querySelectorAll('.version-value');
+ assert.ok(values.some(value => value.textContent === label && value.title === label));
+});
+
+test('mismatch warnings keep emphasis around literal preset and engine names', () => {
+ const d = loadDashboard();
+ const warning = d.detectPresetMismatches(run)[0];
+ assert.equal(warning.textContent, `⚠ Preset ${preset} configured physics as ${label}, but Physics & Simulation was actually used.`);
+ assert.equal(warning.querySelectorAll('strong').length, 3);
+ assert.equal(warning.className, 'warning-line');
+});
+
+test('table headings preserve the preset parts as styled text', () => {
+ const d = loadDashboard();
+ const table = d.buildFullTable('FPS', ['FPS'], [preset], { FPS: { [preset]: 12 } }, true);
+ const physics = table.querySelector('.physics');
+ assert.equal(physics?.textContent, label);
+ assert.equal(table.querySelector('.renderer')?.textContent, 'Renderer');
+ assert.equal(table.querySelector('.datatype')?.textContent, 'RGB');
+});
+
+test('ranking text retains environment and memory details plus the copy action', () => {
+ const d = loadDashboard();
+ const card = d.buildFpsRankCard('FPS', [preset], {
+ FPS: { [preset]: 12 }, 'GPU Memory Used': { [preset]: 2 },
+ }, 'benchmark_non_rl', { [preset]: 512 }, {}, {}, {}, {}, {}, run);
+ assert.ok(card.querySelector('.rank-label')?.textContent.includes(label));
+ assert.equal(card.querySelector('.rank-envs')?.textContent, '(512 envs)');
+ assert.equal(card.querySelector('.rank-vram')?.textContent, '2.00 GB VRAM');
+ assert.equal(typeof card.querySelector('.copy-cmd-btn')?.listeners.click, 'function');
+});
+
+test('history summary values and labels are text', () => {
+ const d = loadDashboard();
+ d.state.histWorkflow = 'benchmark_non_rl';
+ d.state.histMetric = 'FPS';
+ d.showHistRunDetail({ runs: [run] }, 0);
+ const summary = d.document.querySelector('#histSummaryContent');
+ assert.equal(summary.querySelector('.label')?.textContent, `${label} / Renderer / RGB`);
+ assert.equal(summary.querySelector('.value')?.textContent, '12.00');
+});
+
+test('status columns retain literal task names and their header structure', () => {
+ const d = loadDashboard();
+ d.renderBenchmarkStatusGrid({ runs: [run] });
+ const grid = d.document.querySelector('#benchmarkStatusGrid');
+ assert.equal(grid.querySelector('.status-task-name')?.textContent, label);
+ assert.equal(grid.querySelector('.status-col-header')?.children.length, 2);
+});