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,
+ )}