diff --git a/src/imgtests/web/static/css/style.css b/src/imgtests/web/static/css/style.css index 85d91a1c..acc5ab61 100644 --- a/src/imgtests/web/static/css/style.css +++ b/src/imgtests/web/static/css/style.css @@ -403,3 +403,32 @@ h1 i { color: rgba(255, 255, 255, 0.9); margin: 0; } + +.duration-input-group { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + gap: 8px; + margin-top: 6px; +} + +.duration-input-field { + display: flex; + flex-direction: column; + gap: 4px; + color: #666; + font-size: 0.85em; +} + +.duration-input-field input { + width: 78px; + padding: 4px; +} + +.duration-input-field:first-child input { + width: 92px; +} + +.duration-input-error { + border: 2px solid #c62828; +} diff --git a/src/imgtests/web/static/js/duration.js b/src/imgtests/web/static/js/duration.js new file mode 100644 index 00000000..0eb89537 --- /dev/null +++ b/src/imgtests/web/static/js/duration.js @@ -0,0 +1,140 @@ +(function () { + "use strict"; + + const units = [ + { key: "days", label: "Days", max: 9999, multiplier: 86400 }, + { key: "hours", label: "Hours", max: 23, multiplier: 3600 }, + { key: "minutes", label: "Minutes", max: 59, multiplier: 60 }, + { key: "seconds", label: "Seconds", max: 59, multiplier: 1 }, + ]; + + const maxTotalSeconds = units.reduce( + (total, unit) => total + unit.max * unit.multiplier, + 0, + ); + + function validatePrefix(prefix) { + if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(prefix)) { + throw new Error(`Invalid duration input prefix: ${prefix}`); + } + } + + function normalizeTotalSeconds(totalSeconds) { + const parsed = Number.parseInt(totalSeconds, 10); + if (!Number.isFinite(parsed) || parsed < 0) { + return 0; + } + return Math.min(parsed, maxTotalSeconds); + } + + function split(totalSeconds) { + let remaining = normalizeTotalSeconds(totalSeconds); + const values = {}; + + units.forEach((unit) => { + values[unit.key] = Math.floor(remaining / unit.multiplier); + remaining %= unit.multiplier; + }); + + return values; + } + + function render(prefix, totalSeconds = 0, disabled = false) { + validatePrefix(prefix); + const values = split(totalSeconds); + const disabledAttribute = disabled ? " disabled" : ""; + + const inputs = units + .map( + (unit) => ` + + `, + ) + .join(""); + + return ` +
+ ${inputs} +
+ `; + } + + function getComponent(prefix, unit) { + const input = document.getElementById(`${prefix}_${unit.key}`); + if (!input) { + throw new Error(`Duration field ${unit.label.toLowerCase()} was not found.`); + } + + const rawValue = input.value.trim(); + const value = rawValue === "" ? 0 : Number(rawValue); + const isValid = Number.isInteger(value) && value >= 0 && value <= unit.max; + + input.classList.toggle("duration-input-error", !isValid); + if (!isValid) { + input.focus(); + throw new Error(`${unit.label} must be an integer from 0 to ${unit.max}.`); + } + + return value; + } + + function toSeconds(prefix, fieldLabel = "Duration") { + validatePrefix(prefix); + const total = units.reduce( + (sum, unit) => sum + getComponent(prefix, unit) * unit.multiplier, + 0, + ); + + if (total === 0) { + document.getElementById(`${prefix}_seconds`)?.focus(); + throw new Error(`${fieldLabel} must be greater than zero.`); + } + + return total; + } + + function setDisabled(prefix, disabled) { + validatePrefix(prefix); + units.forEach((unit) => { + const input = document.getElementById(`${prefix}_${unit.key}`); + if (input) { + input.disabled = disabled; + } + }); + } + + function format(totalSeconds) { + const values = split(totalSeconds); + const parts = []; + + if (values.days > 0) parts.push(`${values.days}d`); + if (values.hours > 0) parts.push(`${values.hours}h`); + if (values.minutes > 0) parts.push(`${values.minutes}m`); + if (values.seconds > 0 || parts.length === 0) parts.push(`${values.seconds}s`); + + return parts.join(" "); + } + + window.DurationInput = Object.freeze({ + MAX_TOTAL_SECONDS: maxTotalSeconds, + format, + render, + setDisabled, + split, + toSeconds, + }); +})(); diff --git a/src/imgtests/web/static/js/launch_test.js b/src/imgtests/web/static/js/launch_test.js index 1d1ddfc9..a19a3900 100644 --- a/src/imgtests/web/static/js/launch_test.js +++ b/src/imgtests/web/static/js/launch_test.js @@ -22,11 +22,16 @@ function getSelectedSubsystems() { function collectSingleConfig() { return { - durations: { duration_sec: parseInt(document.getElementById("duration_sec").value, 10) }, + durations: { + duration_sec: DurationInput.toSeconds( + "profiled_single_duration", + "Profile duration", + ), + }, profile: document.getElementById("profileSelect").value, pattern: document.getElementById("profiledPatternSelect").value, subsystems: getSelectedSubsystems(), - run_matrix: false + run_matrix: false, }; } @@ -37,10 +42,13 @@ function collectMatrixConfig() { document.querySelectorAll('#profilesCheckboxes input[type="checkbox"]').forEach(cb => { if (cb.checked) { matrixProfiles.push(cb.value); - const durInput = document.getElementById(`duration_${cb.value}`); - if (durInput) { - durations[`duration_${cb.value}`] = parseInt(durInput.value, 10); - } + const durationPrefix = + cb.dataset.durationPrefix || + `profiled_${cb.value}_duration`; + durations[`duration_${cb.value}`] = DurationInput.toSeconds( + durationPrefix, + `${cb.value} profile duration`, + ); } }); @@ -66,12 +74,24 @@ document.getElementById("runTestsBtn").addEventListener("click", function () { : 1; let config = null; - if (testing_mode === "profiled") { - const conf_mode = document.querySelector('input[name="profiledConfigMode"]:checked').value; - if (conf_mode === "custom") { - const runMode = document.querySelector('input[name="profiledRunMode"]:checked').value; - config = runMode === "single" ? collectSingleConfig() : collectMatrixConfig(); + try { + if (testing_mode === "profiled") { + const conf_mode = document.querySelector( + 'input[name="profiledConfigMode"]:checked', + ).value; + if (conf_mode === "custom") { + const runMode = document.querySelector( + 'input[name="profiledRunMode"]:checked', + ).value; + config = + runMode === "single" + ? collectSingleConfig() + : collectMatrixConfig(); + } } + } catch (error) { + outputContainer.textContent = "Error: " + error.message; + return; } btn.disabled = true; diff --git a/src/imgtests/web/static/js/test_config.js b/src/imgtests/web/static/js/test_config.js index 79dd4971..2e1e23cb 100644 --- a/src/imgtests/web/static/js/test_config.js +++ b/src/imgtests/web/static/js/test_config.js @@ -80,46 +80,54 @@ class TestConfigManager { console.log("Found radio buttons:", radioButtons.length); radioButtons.forEach((radio) => { - radio.addEventListener("change", (e) => { - console.log("Config mode changed to:", e.target.value); + radio.addEventListener("change", (event) => { + console.log("Config mode changed to:", event.target.value); const panel = document.getElementById("customConfigPanel"); if (panel) { panel.style.display = - e.target.value === "custom" ? "block" : "none"; - if (e.target.value === "custom" && this.availableSuites) { + event.target.value === "custom" ? "block" : "none"; + if ( + event.target.value === "custom" && + this.availableSuites + ) { this.renderConfigUI(); } } }); - document.querySelectorAll('input[name="profiledConfigMode"]').forEach(radio => { - radio.addEventListener("change", e => { - const panel = document.getElementById("profiledCustomPanel"); - if (e.target.value === "custom") { - panel.style.display = "block"; - this.renderProfilesAndDurationsUI(); - this.renderPatternUI(); - this.renderSubsystemsUI(); - } else { - panel.style.display = "none"; - } - }); }); - document.querySelectorAll('input[name="profiledRunMode"]').forEach(radio => { - radio.addEventListener("change", e => { - const singlePanel = document.getElementById("singleRunPanel"); - const matrixPanel = document.getElementById("matrixRunPanel"); - if (e.target.value === "single") { - singlePanel.style.display = "block"; - matrixPanel.style.display = "none"; - } else { - singlePanel.style.display = "none"; - matrixPanel.style.display = "block"; - } - this.renderProfilesAndDurationsUI(); + + document + .querySelectorAll('input[name="profiledConfigMode"]') + .forEach((radio) => { + radio.addEventListener("change", (event) => { + const panel = + document.getElementById("profiledCustomPanel"); + if (event.target.value === "custom") { + panel.style.display = "block"; + this.renderProfilesAndDurationsUI(); + this.renderPatternUI(); + this.renderSubsystemsUI(); + } else { + panel.style.display = "none"; + } + }); }); - }); - }); + document + .querySelectorAll('input[name="profiledRunMode"]') + .forEach((radio) => { + radio.addEventListener("change", (event) => { + const singlePanel = + document.getElementById("singleRunPanel"); + const matrixPanel = + document.getElementById("matrixRunPanel"); + const isSingle = event.target.value === "single"; + + singlePanel.style.display = isSingle ? "block" : "none"; + matrixPanel.style.display = isSingle ? "none" : "block"; + this.renderProfilesAndDurationsUI(); + }); + }); const saveBtn = document.getElementById("saveConfigBtn"); if (saveBtn) { @@ -180,8 +188,10 @@ class TestConfigManager { this.currentConfig.suite_durations?.[suiteName] || suiteInfo.default_duration || 300; + const durationPrefix = `basic_${suiteName}_duration`; const div = document.createElement("div"); + div.className = "suite-config-card"; div.style.margin = "15px 0"; div.style.padding = "10px"; div.style.border = "1px solid #e0e0e0"; @@ -193,6 +203,7 @@ class TestConfigManager {
@@ -203,22 +214,20 @@ class TestConfigManager {
- Default duration: ${suiteInfo.default_duration}s | + Default duration: + ${DurationInput.format(suiteInfo.default_duration)} | Tests: ${suiteInfo.test_count || 0}
-
- +
+
Custom duration:
+ ${DurationInput.render( + durationPrefix, + currentDuration, + !isChecked, + )}