diff --git a/src/imgtests/web/locale/ru/LC_MESSAGES/djangojs.po b/src/imgtests/web/locale/ru/LC_MESSAGES/djangojs.po index 222217bf..962fd11c 100644 --- a/src/imgtests/web/locale/ru/LC_MESSAGES/djangojs.po +++ b/src/imgtests/web/locale/ru/LC_MESSAGES/djangojs.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: imgtests 0.0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-21 15:45+0300\n" +"POT-Creation-Date: 2026-08-02 18:38+0500\n" "PO-Revision-Date: 2026-07-21 15:45+0300\n" "Last-Translator: Nikita Pivoev nikita.pivoev@yandex.ru\n" "Language: ru\n" @@ -50,28 +50,82 @@ msgstr "Введите описание (необязательно):" msgid "Run tests for %(display_name)s platform" msgstr "Запустить тесты для платформы %(display_name)s" -#: src/imgtests/web/static/js/dashboard.js:98 -#: src/imgtests/web/static/js/dashboard.js:117 -#: src/imgtests/web/static/js/dashboard.js:142 -#: src/imgtests/web/static/js/excel_reports.js:55 -#: src/imgtests/web/static/js/excel_reports.js:61 -#: src/imgtests/web/static/js/html_reports.js:63 -#: src/imgtests/web/static/js/launch_test.js:103 -#: src/imgtests/web/static/js/launch_test.js:111 +#: src/imgtests/web/static/js/dashboard.js:99 +#: src/imgtests/web/static/js/dashboard.js:122 +#: src/imgtests/web/static/js/dashboard.js:151 +#: src/imgtests/web/static/js/excel_reports.js:56 +#: src/imgtests/web/static/js/excel_reports.js:66 +#: src/imgtests/web/static/js/html_reports.js:64 msgid "Error: %(error)s" msgstr "Ошибка: %(error)s" -#: src/imgtests/web/static/js/dashboard.js:105 +#: src/imgtests/web/static/js/dashboard.js:109 msgid "Are you sure you want to remove this distribution?" msgstr "Вы уверены, что хотите удалить данный дистрибутив?" -#: src/imgtests/web/static/js/dashboard.js:126 +#: src/imgtests/web/static/js/dashboard.js:134 msgid "" "Reset to default distributions? This will remove all custom distributions." msgstr "" "Восстановить изначальные дистрибутивы? Это удалит все пользовательские " "дистрибутивы." +#: src/imgtests/web/static/js/duration.js:7 +msgid "Days" +msgstr "Дни" + +#: src/imgtests/web/static/js/duration.js:13 +msgid "Hours" +msgstr "Часы" + +#: src/imgtests/web/static/js/duration.js:19 +msgid "Minutes" +msgstr "Минуты" + +#: src/imgtests/web/static/js/duration.js:25 +msgid "Seconds" +msgstr "Секунды" + +#: src/imgtests/web/static/js/duration.js:82 +msgid "Allowed range: 0-%(max)s" +msgstr "Допустимый диапазон: 0-%(max)s" + +#: src/imgtests/web/static/js/duration.js:105 +msgid "Duration field %(field)s was not found." +msgstr "Поле длительности «%(field)s» не найдено." + +#: src/imgtests/web/static/js/duration.js:121 +msgid "%(field)s must be an integer from 0 to %(max)s." +msgstr "Значение «%(field)s» должно быть целым числом от 0 до %(max)s." + +#: src/imgtests/web/static/js/duration.js:133 +msgid "Duration" +msgstr "Длительность" + +#: src/imgtests/web/static/js/duration.js:143 +msgid "%(field)s must be greater than zero." +msgstr "Значение «%(field)s» должно быть больше нуля." + +#: src/imgtests/web/static/js/duration.js:168 +msgctxt "duration unit" +msgid "d" +msgstr "д" + +#: src/imgtests/web/static/js/duration.js:170 +msgctxt "duration unit" +msgid "h" +msgstr "ч" + +#: src/imgtests/web/static/js/duration.js:172 +msgctxt "duration unit" +msgid "m" +msgstr "м" + +#: src/imgtests/web/static/js/duration.js:174 +msgctxt "duration unit" +msgid "s" +msgstr "с" + #: src/imgtests/web/static/js/excel_reports.js:30 msgid "Please select at least one table." msgstr "Пожалуйста, выберите хотя бы одну таблицу." @@ -85,8 +139,8 @@ msgstr "Пожалуйста, выберите хотя бы один дистр msgid "Generating..." msgstr "Создание..." -#: src/imgtests/web/static/js/excel_reports.js:57 -#: src/imgtests/web/static/js/excel_reports.js:63 +#: src/imgtests/web/static/js/excel_reports.js:61 +#: src/imgtests/web/static/js/excel_reports.js:71 msgid "Generate New Excel Report" msgstr "Создать новый Excel-отчет" @@ -98,175 +152,194 @@ msgstr "(нет описания)" msgid "Report generated! Reload page to see it" msgstr "Отчет создан! Перезагрузите страницу, чтобы увидеть его" -#: src/imgtests/web/static/js/html_reports.js:63 +#: src/imgtests/web/static/js/html_reports.js:65 msgid "unknown" msgstr "неизвестно" -#: src/imgtests/web/static/js/html_reports.js:71 +#: src/imgtests/web/static/js/html_reports.js:76 msgid "Request failed: %(message)s" msgstr "Запрос не удался: %(message)s" -#: src/imgtests/web/static/js/html_reports.js:77 +#: src/imgtests/web/static/js/html_reports.js:85 msgid "Generate" msgstr "Создать" -#: src/imgtests/web/static/js/launch_test.js:78 +#: src/imgtests/web/static/js/launch_test.js:31 +msgid "Profile duration" +msgstr "Длительность профиля" + +#: src/imgtests/web/static/js/launch_test.js:57 +msgid "%(profile)s profile duration" +msgstr "Длительность профиля %(profile)s" + +#: src/imgtests/web/static/js/launch_test.js:103 +#: src/imgtests/web/static/js/launch_test.js:137 +#: src/imgtests/web/static/js/launch_test.js:150 +msgid "Error: %(message)s" +msgstr "Ошибка: %(message)s" + +#: src/imgtests/web/static/js/launch_test.js:111 msgid "Running..." msgstr "Выполнение..." -#: src/imgtests/web/static/js/launch_test.js:79 -msgid "Tests Running..." +#: src/imgtests/web/static/js/launch_test.js:112 +msgid "Tests running..." msgstr "Тесты выполняются..." -#: src/imgtests/web/static/js/launch_test.js:97 +#: src/imgtests/web/static/js/launch_test.js:130 msgid "Tests running... (Task ID: %(task_id)s)" msgstr "Тесты выполняются... (ID задачи: %(task_id)s)" -#: src/imgtests/web/static/js/launch_test.js:104 +#: src/imgtests/web/static/js/launch_test.js:140 msgid "Failed to start tests" msgstr "Не удалось запустить тесты" -#: src/imgtests/web/static/js/launch_test.js:107 -#: src/imgtests/web/static/js/launch_test.js:113 -#: src/imgtests/web/static/js/launch_test.js:133 -#: src/imgtests/web/static/js/launch_test.js:144 -#: src/imgtests/web/static/js/launch_test.js:148 -#: src/imgtests/web/static/js/launch_test.js:157 +#: src/imgtests/web/static/js/launch_test.js:145 +#: src/imgtests/web/static/js/launch_test.js:155 +#: src/imgtests/web/static/js/launch_test.js:165 msgid "Run tests" msgstr "Запустить тесты" -#: src/imgtests/web/static/js/launch_test.js:127 +#: src/imgtests/web/static/js/launch_test.js:174 msgid "Tests running... Please wait." msgstr "Тесты выполняются... Пожалуйста, подождите." -#: src/imgtests/web/static/js/launch_test.js:131 +#: src/imgtests/web/static/js/launch_test.js:180 msgid "Tests completed successfully." msgstr "Тесты завершены успешно." -#: src/imgtests/web/static/js/launch_test.js:135 +#: src/imgtests/web/static/js/launch_test.js:183 msgid "Test failed" msgstr "Тест не пройден" -#: src/imgtests/web/static/js/launch_test.js:146 +#: src/imgtests/web/static/js/launch_test.js:187 +msgid "Error output:" +msgstr "Вывод ошибок:" + +#: src/imgtests/web/static/js/launch_test.js:193 +msgid "Output:" +msgstr "Вывод:" + +#: src/imgtests/web/static/js/launch_test.js:198 msgid "Unknown status" msgstr "Неизвестный статус" -#: src/imgtests/web/static/js/launch_test.js:153 -msgid "Error checking status: %(error)s" -msgstr "Ошибка проверки статуса: %(error)s" +#: src/imgtests/web/static/js/launch_test.js:204 +msgid "Error checking status: %(message)s" +msgstr "Ошибка проверки статуса: %(message)s" -#: src/imgtests/web/static/js/test_config.js:21 +#: src/imgtests/web/static/js/test_config.js:22 msgid "Failed to load configuration: %(message)s" msgstr "Не удалось загрузить конфигурацию: %(message)s" -#: src/imgtests/web/static/js/test_config.js:44 +#: src/imgtests/web/static/js/test_config.js:49 msgid "Failed to load test suites: %(message)s" msgstr "Не удалось загрузить тестовые сценарии: %(message)s" -#: src/imgtests/web/static/js/test_config.js:64 +#: src/imgtests/web/static/js/test_config.js:73 msgid "Failed to load tests for %(distro_name)s" msgstr "Не удалось загрузить тесты для %(distro_name)s" -#: src/imgtests/web/static/js/test_config.js:150 +#: src/imgtests/web/static/js/test_config.js:171 msgid "Failed to load test suites. Please refresh the page." msgstr "Не удалось загрузить тестовые сценарии. Пожалуйста, обновите страницу." -#: src/imgtests/web/static/js/test_config.js:173 +#: src/imgtests/web/static/js/test_config.js:196 msgid "No test suites available" msgstr "Нет доступных тестовых сценариев" -#: src/imgtests/web/static/js/test_config.js:204 +#: src/imgtests/web/static/js/test_config.js:230 msgid "No description" msgstr "(нет описания)" -#: src/imgtests/web/static/js/test_config.js:208 +#: src/imgtests/web/static/js/test_config.js:234 msgid "Default duration:" msgstr "Продолжительность по умолчанию:" -#: src/imgtests/web/static/js/test_config.js:209 +#: src/imgtests/web/static/js/test_config.js:236 msgid "Tests:" msgstr "Тесты:" -#: src/imgtests/web/static/js/test_config.js:213 -msgid "Custom duration (seconds):" -msgstr "Продолжительность (секунды):" +#: src/imgtests/web/static/js/test_config.js:240 +msgid "Custom duration:" +msgstr "Пользовательская длительность:" -#: src/imgtests/web/static/js/test_config.js:226 +#: src/imgtests/web/static/js/test_config.js:250 msgid "Select Individual Tests" msgstr "Выбрать отдельные тесты" -#: src/imgtests/web/static/js/test_config.js:231 -msgid "test(s) selected" -msgstr "тест(ов) выбрано" +#: src/imgtests/web/static/js/test_config.js:256 +msgid "%(count)s test(s) selected" +msgstr "Выбрано тестов: %(count)s" -#: src/imgtests/web/static/js/test_config.js:303 +#: src/imgtests/web/static/js/test_config.js:336 msgid "Select tests for" msgstr "Выберите тесты для" -#: src/imgtests/web/static/js/test_config.js:304 +#: src/imgtests/web/static/js/test_config.js:337 msgid "Leave all unchecked to run all tests" msgstr "Оставьте все неотмеченными, чтобы запустить все тесты" -#: src/imgtests/web/static/js/test_config.js:321 +#: src/imgtests/web/static/js/test_config.js:354 msgid "Save Selection" msgstr "Сохранить выбор" -#: src/imgtests/web/static/js/test_config.js:322 +#: src/imgtests/web/static/js/test_config.js:355 msgid "Cancel" msgstr "Отмена" -#: src/imgtests/web/static/js/test_config.js:345 +#: src/imgtests/web/static/js/test_config.js:380 msgid "Selected %(count)s tests for %(suite_name)s" msgstr "Выбрано %(count)s тестов для %(suite_name)s" -#: src/imgtests/web/static/js/test_config.js:354 +#: src/imgtests/web/static/js/test_config.js:395 msgid "Will run all tests for %(suite_name)s" msgstr "Будут запущены все тесты для %(suite_name)s" -#: src/imgtests/web/static/js/test_config.js:375 +#: src/imgtests/web/static/js/test_config.js:420 msgid "Failed to load tests for %(suite_name)s" msgstr "Не удалось загрузить тесты для %(suite_name)s" -#: src/imgtests/web/static/js/test_config.js:433 +#: src/imgtests/web/static/js/test_config.js:449 +msgid "%(suite)s duration" +msgstr "Длительность набора %(suite)s" + +#: src/imgtests/web/static/js/test_config.js:485 msgid "Configuration saved successfully!" msgstr "Конфигурация успешно сохранена!" -#: src/imgtests/web/static/js/test_config.js:443 +#: src/imgtests/web/static/js/test_config.js:498 msgid "Error saving configuration: %(message)s" msgstr "Ошибка сохранения конфигурации: %(message)s" -#: src/imgtests/web/static/js/test_config.js:487 +#: src/imgtests/web/static/js/test_config.js:546 msgid "Configuration reset to default" msgstr "Конфигурация сброшена до значений по умолчанию" -#: src/imgtests/web/static/js/test_config.js:491 +#: src/imgtests/web/static/js/test_config.js:553 msgid "Failed to reset configuration" msgstr "Не удалось сбросить конфигурацию" -#: src/imgtests/web/static/js/test_config.js:548 -msgid "Duration (sec)" -msgstr "Длительность (сек)" - -#: src/imgtests/web/static/js/test_config.js:575 +#: src/imgtests/web/static/js/test_config.js:661 msgid "File" msgstr "Файловая" -#: src/imgtests/web/static/js/test_config.js:576 +#: src/imgtests/web/static/js/test_config.js:662 msgid "IPC" msgstr "IPC" -#: src/imgtests/web/static/js/test_config.js:577 +#: src/imgtests/web/static/js/test_config.js:663 msgid "Memory" msgstr "Память" -#: src/imgtests/web/static/js/test_config.js:578 +#: src/imgtests/web/static/js/test_config.js:664 msgid "Network" msgstr "Сетевая" -#: src/imgtests/web/static/js/test_config.js:579 +#: src/imgtests/web/static/js/test_config.js:665 msgid "Syscalls" msgstr "Системные вызовы" -#: src/imgtests/web/static/js/test_config.js:580 +#: src/imgtests/web/static/js/test_config.js:666 msgid "System" msgstr "Система" 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..29b42025 --- /dev/null +++ b/src/imgtests/web/static/js/duration.js @@ -0,0 +1,187 @@ +(function () { + "use strict"; + + const units = [ + { + key: "days", + getLabel: () => gettext("Days"), + max: 9999, + multiplier: 86400, + }, + { + key: "hours", + getLabel: () => gettext("Hours"), + max: 23, + multiplier: 3600, + }, + { + key: "minutes", + getLabel: () => gettext("Minutes"), + max: 59, + multiplier: 60, + }, + { + key: "seconds", + getLabel: () => gettext("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( + interpolate( + gettext("Duration field %(field)s was not found."), + {field: unit.getLabel().toLowerCase()}, + true, + ), + ); + } + + 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( + interpolate( + gettext("%(field)s must be an integer from 0 to %(max)s."), + {field: unit.getLabel(), max: unit.max}, + true, + ), + ); + } + + return value; + } + + function toSeconds(prefix, fieldLabel) { + validatePrefix(prefix); + const label = fieldLabel || gettext("Duration"); + const total = units.reduce( + (sum, unit) => sum + getComponent(prefix, unit) * unit.multiplier, + 0, + ); + + if (total === 0) { + document.getElementById(`${prefix}_seconds`)?.focus(); + throw new Error( + interpolate( + gettext("%(field)s must be greater than zero."), + {field: label}, + true, + ), + ); + } + + 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 + pgettext("duration unit", "d")); + if (values.hours > 0) + parts.push(values.hours + pgettext("duration unit", "h")); + if (values.minutes > 0) + parts.push(values.minutes + pgettext("duration unit", "m")); + if (values.seconds > 0 || parts.length === 0) + parts.push(values.seconds + pgettext("duration unit", "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 a1fc3161..20da3b6e 100644 --- a/src/imgtests/web/static/js/launch_test.js +++ b/src/imgtests/web/static/js/launch_test.js @@ -16,17 +16,25 @@ function getCookie(name) { } function getSelectedSubsystems() { - return Array.from(document.querySelectorAll('#subsystemsCheckboxes input[type="checkbox"]:checked')) - .map(cb => cb.value); + return Array.from( + document.querySelectorAll( + '#subsystemsCheckboxes input[type="checkbox"]:checked', + ), + ).map((checkbox) => checkbox.value); } function collectSingleConfig() { return { - durations: { duration_sec: parseInt(document.getElementById("duration_sec").value, 10) }, + durations: { + duration_sec: DurationInput.toSeconds( + "profiled_single_duration", + gettext("Profile duration"), + ), + }, profile: document.getElementById("profileSelect").value, pattern: document.getElementById("profiledPatternSelect").value, subsystems: getSelectedSubsystems(), - run_matrix: false + run_matrix: false, }; } @@ -34,22 +42,32 @@ function collectMatrixConfig() { const durations = {}; const matrixProfiles = []; - 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); + document + .querySelectorAll('#profilesCheckboxes input[type="checkbox"]') + .forEach((checkbox) => { + if (checkbox.checked) { + matrixProfiles.push(checkbox.value); + const durationPrefix = + checkbox.dataset.durationPrefix || + `profiled_${checkbox.value}_duration`; + durations[`duration_${checkbox.value}`] = + DurationInput.toSeconds( + durationPrefix, + interpolate( + gettext("%(profile)s profile duration"), + {profile: checkbox.value}, + true, + ), + ); } - } - }); + }); return { durations: durations, matrix_profiles: matrixProfiles, pattern: document.getElementById("profiledPatternSelect").value, subsystems: getSelectedSubsystems(), - run_matrix: true + run_matrix: true, }; } @@ -58,25 +76,40 @@ const csrfToken = getCookie("csrftoken"); document.getElementById("runTestsBtn").addEventListener("click", function () { const btn = this; const outputContainer = document.getElementById("outputContainer"); - - const runner = document.getElementById('configRunner').value; + const runner = document.getElementById("configRunner").value; const testRunsCountInput = document.getElementById("testRunsCount"); const testRunsCount = testRunsCountInput ? parseInt(testRunsCountInput.value, 10) : 1; let config = null; - if (runner === "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 (runner === "profiled") { + const configMode = document.querySelector( + 'input[name="profiledConfigMode"]:checked', + ).value; + if (configMode === "custom") { + const runMode = document.querySelector( + 'input[name="profiledRunMode"]:checked', + ).value; + config = + runMode === "single" + ? collectSingleConfig() + : collectMatrixConfig(); + } } + } catch (error) { + outputContainer.textContent = interpolate( + gettext("Error: %(message)s"), + {message: error.message}, + true, + ); + return; } btn.disabled = true; btn.textContent = gettext("Running..."); - outputContainer.textContent = gettext("Tests Running..."); + outputContainer.textContent = gettext("Tests running..."); fetch("/run-tests/", { method: "POST", @@ -96,14 +129,17 @@ document.getElementById("runTestsBtn").addEventListener("click", function () { outputContainer.textContent = interpolate( gettext("Tests running... (Task ID: %(task_id)s)"), {task_id: data.task_id}, - true + true, ); pollStatus(data.task_id); } else { outputContainer.textContent = interpolate( - gettext("Error: %(error)s"), - {error: data.error || gettext("Failed to start tests")}, - true + gettext("Error: %(message)s"), + { + message: + data.error || gettext("Failed to start tests"), + }, + true, ); btn.disabled = false; btn.textContent = gettext("Run tests"); @@ -111,9 +147,9 @@ document.getElementById("runTestsBtn").addEventListener("click", function () { }) .catch((error) => { outputContainer.textContent = interpolate( - gettext("Error: %(error)s"), - {error: error}, - true + gettext("Error: %(message)s"), + {message: error.message || String(error)}, + true, ); btn.disabled = false; btn.textContent = gettext("Run tests"); @@ -124,44 +160,52 @@ function pollStatus(taskId) { const btn = document.getElementById("runTestsBtn"); const outputContainer = document.getElementById("outputContainer"); + const resetButton = () => { + btn.disabled = false; + btn.textContent = gettext("Run tests"); + }; + const checkStatus = () => { - fetch("/test-status/" + taskId + "/") + fetch(`/test-status/${taskId}/`) .then((response) => response.json()) .then((data) => { if (data.status === "running") { - outputContainer.textContent = - gettext("Tests running... Please wait."); + outputContainer.textContent = gettext( + "Tests running... Please wait.", + ); setTimeout(checkStatus, 2000); } else if (data.status === "completed") { outputContainer.textContent = - data.output || gettext("Tests completed successfully."); - btn.disabled = false; - btn.textContent = gettext("Run tests"); + data.output || + gettext("Tests completed successfully."); + resetButton(); } else if (data.status === "failed") { - let errorMsg = data.error || gettext("Test failed"); + let errorMessage = data.error || gettext("Test failed"); if (data.stderr) { - errorMsg += "\n\nError output:\n" + data.stderr; + errorMessage += + "\n\n" + + gettext("Error output:") + + "\n" + + data.stderr; } if (data.output) { - errorMsg += "\n\nOutput:\n" + data.output; + errorMessage += + "\n\n" + gettext("Output:") + "\n" + data.output; } - outputContainer.textContent = errorMsg; - btn.disabled = false; - btn.textContent = gettext("Run tests"); + outputContainer.textContent = errorMessage; + resetButton(); } else { outputContainer.textContent = gettext("Unknown status"); - btn.disabled = false; - btn.textContent = gettext("Run tests"); + resetButton(); } }) .catch((error) => { outputContainer.textContent = interpolate( - gettext("Error checking status: %(error)s"), - {error: error}, - true + gettext("Error checking status: %(message)s"), + {message: error.message || String(error)}, + true, ); - btn.disabled = false; - btn.textContent = gettext("Run tests"); + resetButton(); }); }; diff --git a/src/imgtests/web/static/js/test_config.js b/src/imgtests/web/static/js/test_config.js index 9599ae4c..a40f5dbc 100644 --- a/src/imgtests/web/static/js/test_config.js +++ b/src/imgtests/web/static/js/test_config.js @@ -21,7 +21,7 @@ class TestConfigManager { interpolate( gettext("Failed to load configuration: %(message)s"), {message: error.message}, - true + true, ), "error", ); @@ -48,7 +48,7 @@ class TestConfigManager { interpolate( gettext("Failed to load test suites: %(message)s"), {message: error.message}, - true + true, ), "error", ); @@ -72,7 +72,7 @@ class TestConfigManager { interpolate( gettext("Failed to load tests for %(distro_name)s"), {distro_name: window.distroName}, - true + true, ), "error", ); @@ -92,46 +92,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) { @@ -159,7 +167,9 @@ class TestConfigManager { if (suitesDiv) { suitesDiv.innerHTML = '

' + - gettext("Failed to load test suites. Please refresh the page.") + + gettext( + "Failed to load test suites. Please refresh the page.", + ) + "

"; } return; @@ -182,7 +192,8 @@ class TestConfigManager { console.log("Rendering suites:", suiteNames); if (suiteNames.length === 0) { - suitesDiv.innerHTML = "

" + gettext("No test suites available") + "

"; + suitesDiv.innerHTML = + "

" + gettext("No test suites available") + "

"; return; } @@ -194,8 +205,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"; @@ -207,6 +220,7 @@ class TestConfigManager {
@@ -217,22 +231,20 @@ class TestConfigManager {
- ${gettext("Default duration:")} ${suiteInfo.default_duration}s | + ${gettext("Default duration:")} + ${DurationInput.format(suiteInfo.default_duration)} | ${gettext("Tests:")} ${suiteInfo.test_count || 0}
-
- +
+
${gettext("Custom duration:")}
+ ${DurationInput.render( + durationPrefix, + currentDuration, + !isChecked, + )}