Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/imgtests/web/static/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
140 changes: 140 additions & 0 deletions src/imgtests/web/static/js/duration.js
Original file line number Diff line number Diff line change
@@ -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) => `
<label class="duration-input-field" for="${prefix}_${unit.key}">
<span>${unit.label}</span>
<input
class="duration-component"
type="number"
id="${prefix}_${unit.key}"
value="${values[unit.key]}"
min="0"
max="${unit.max}"
step="1"
inputmode="numeric"
title="Allowed range: 0-${unit.max}"
${disabledAttribute}
>
</label>
`,
)
.join("");

return `
<div class="duration-input-group" data-duration-prefix="${prefix}">
${inputs}
</div>
`;
}

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,
});
})();
42 changes: 31 additions & 11 deletions src/imgtests/web/static/js/launch_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}

Expand All @@ -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`,
);
}
});

Expand All @@ -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;
Expand Down
Loading
Loading