release: Multi-Hoster-Upload v2.0.4
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-hoster-uploader",
|
"name": "multi-hoster-uploader",
|
||||||
"version": "2.0.3",
|
"version": "2.0.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "multi-hoster-uploader",
|
"name": "multi-hoster-uploader",
|
||||||
"version": "2.0.3",
|
"version": "2.0.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^3.6.0",
|
"chokidar": "^3.6.0",
|
||||||
"undici": "^7.29.0",
|
"undici": "^7.29.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-hoster-uploader",
|
"name": "multi-hoster-uploader",
|
||||||
"version": "2.0.3",
|
"version": "2.0.4",
|
||||||
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
(function (scope) {
|
||||||
|
const STATUS_PRESENTATIONS = {
|
||||||
|
ok: { statusClass: 'ok', label: 'Bereit', requiresOtp: false },
|
||||||
|
warn: { statusClass: 'warn', label: 'Warnung', requiresOtp: false },
|
||||||
|
checking: { statusClass: 'checking', label: 'Prüfe...', requiresOtp: false },
|
||||||
|
error: { statusClass: 'error', label: 'Fehler', requiresOtp: false },
|
||||||
|
otp_required: { statusClass: 'warn', label: 'OTP erforderlich', requiresOtp: true },
|
||||||
|
unchecked: { statusClass: 'unchecked', label: 'Nicht geprüft', requiresOtp: false },
|
||||||
|
disabled: { statusClass: 'disabled', label: 'Deaktiviert', requiresOtp: false }
|
||||||
|
};
|
||||||
|
|
||||||
|
function getAccountStatusPresentation(status) {
|
||||||
|
const presentation = STATUS_PRESENTATIONS[status] || STATUS_PRESENTATIONS.unchecked;
|
||||||
|
return { ...presentation };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAccountGroupStatus(summary) {
|
||||||
|
const total = Math.max(0, Number(summary && summary.total) || 0);
|
||||||
|
const disabled = Math.min(total, Math.max(0, Number(summary && summary.disabled) || 0));
|
||||||
|
const active = total - disabled;
|
||||||
|
const errors = Math.max(0, Number(summary && summary.error) || 0);
|
||||||
|
const ok = Math.max(0, Number(summary && summary.ok) || 0);
|
||||||
|
const warning = Math.max(0, Number(summary && summary.warn) || 0);
|
||||||
|
const checking = Math.max(0, Number(summary && summary.checking) || 0);
|
||||||
|
const unchecked = Math.max(0, Number(summary && summary.unchecked) || 0);
|
||||||
|
if (active === 0) return 'unchecked';
|
||||||
|
if (errors >= active) return 'error';
|
||||||
|
if (errors > 0 || warning > 0 || checking > 0 || unchecked > 0) return 'warn';
|
||||||
|
if (ok >= active) return 'ok';
|
||||||
|
return 'warn';
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountStatus = { getAccountGroupStatus, getAccountStatusPresentation };
|
||||||
|
if (typeof module !== 'undefined' && module.exports) module.exports = accountStatus;
|
||||||
|
if (scope) scope.AccountStatus = accountStatus;
|
||||||
|
})(typeof window !== 'undefined' ? window : globalThis);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
(function (scope) {
|
(function (scope) {
|
||||||
function getAccountSubmitLabel({ isEdit } = {}) {
|
function getAccountSubmitLabel() {
|
||||||
return isEdit ? 'Prüfen und speichern' : 'Prüfen und anlegen';
|
return 'Prüfen und speichern';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitValidatedAccount({ validate, commit, afterCommit, isCurrent }) {
|
async function submitValidatedAccount({ validate, commit, afterCommit, isCurrent }) {
|
||||||
|
|||||||
+57
-15
@@ -3920,13 +3920,12 @@ function getCredentialLabel(name, account) {
|
|||||||
return 'Keine Zugangsdaten';
|
return 'Keine Zugangsdaten';
|
||||||
}
|
}
|
||||||
|
|
||||||
const _STATUS_LABELS = { ok: 'Bereit', warn: 'Warnung', checking: 'Prüfe...', error: 'Fehler', unchecked: 'Nicht geprüft' };
|
|
||||||
|
|
||||||
function _buildAccountCardHtml(name, account, idx) {
|
function _buildAccountCardHtml(name, account, idx) {
|
||||||
const isDisabled = account.enabled === false;
|
const isDisabled = account.enabled === false;
|
||||||
const st = accountStatuses[account.id] || { status: 'unchecked', message: '' };
|
const st = accountStatuses[account.id] || { status: 'unchecked', message: '' };
|
||||||
const statusLabel = isDisabled ? 'Deaktiviert' : (_STATUS_LABELS[st.status] || 'Nicht geprüft');
|
const statusPresentation = window.AccountStatus.getAccountStatusPresentation(isDisabled ? 'disabled' : st.status);
|
||||||
const statusClass = isDisabled ? 'disabled' : st.status;
|
const statusLabel = statusPresentation.label;
|
||||||
|
const statusClass = statusPresentation.statusClass;
|
||||||
const credLabel = getCredentialLabel(name, account);
|
const credLabel = getCredentialLabel(name, account);
|
||||||
const userLabel = account.label && String(account.label).trim();
|
const userLabel = account.label && String(account.label).trim();
|
||||||
// Subtitle: "Label: XYZ • API: ABC… • <status>" — the user-set label is the
|
// Subtitle: "Label: XYZ • API: ABC… • <status>" — the user-set label is the
|
||||||
@@ -3940,6 +3939,12 @@ function _buildAccountCardHtml(name, account, idx) {
|
|||||||
const sessionPausedBadge = isSessionPaused
|
const sessionPausedBadge = isSessionPaused
|
||||||
? `<span class="account-session-paused" title="Account wurde diese Session als fehlerhaft markiert. Klick = Wieder als aktiv markieren.">Pausiert (Session) <button class="account-session-reactivate" data-account-reactivate="${account.id}" data-account-reactivate-hoster="${name}" title="Wieder aktivieren">↻</button></span>`
|
? `<span class="account-session-paused" title="Account wurde diese Session als fehlerhaft markiert. Klick = Wieder als aktiv markieren.">Pausiert (Session) <button class="account-session-reactivate" data-account-reactivate="${account.id}" data-account-reactivate-hoster="${name}" title="Wieder aktivieren">↻</button></span>`
|
||||||
: '';
|
: '';
|
||||||
|
const otpAction = !isDisabled && statusPresentation.requiresOtp
|
||||||
|
? `<div class="account-otp-action">
|
||||||
|
<input class="key-input account-otp-input" data-account-otp-input="${escapeAttr(account.id)}" type="text" inputmode="numeric" autocomplete="one-time-code" maxlength="10" placeholder="Code aus E-Mail">
|
||||||
|
<button class="btn btn-xs btn-primary" data-account-otp-submit="${escapeAttr(account.id)}">Prüfen und speichern</button>
|
||||||
|
</div>`
|
||||||
|
: '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="account-card${isDisabled ? ' account-disabled' : ''}${isSessionPaused ? ' account-session-paused-card' : ''}" data-account-id="${account.id}" data-account-hoster="${name}" draggable="true">
|
<div class="account-card${isDisabled ? ' account-disabled' : ''}${isSessionPaused ? ' account-session-paused-card' : ''}" data-account-id="${account.id}" data-account-hoster="${name}" draggable="true">
|
||||||
@@ -3947,6 +3952,7 @@ function _buildAccountCardHtml(name, account, idx) {
|
|||||||
<div class="account-card-info">
|
<div class="account-card-info">
|
||||||
<div class="account-card-title">${escapeHtml(getAccountDisplayName(name, account))} <span class="account-priority-badge">${priorityLabel}</span> ${sessionPausedBadge}</div>
|
<div class="account-card-title">${escapeHtml(getAccountDisplayName(name, account))} <span class="account-priority-badge">${priorityLabel}</span> ${sessionPausedBadge}</div>
|
||||||
<div class="account-card-subtitle" title="${escapeAttr(subtitleText)}">${escapeHtml(subtitleText)}${st.message && !isDisabled ? ` • ${escapeHtml(st.message)}` : ''}</div>
|
<div class="account-card-subtitle" title="${escapeAttr(subtitleText)}">${escapeHtml(subtitleText)}${st.message && !isDisabled ? ` • ${escapeHtml(st.message)}` : ''}</div>
|
||||||
|
${otpAction}
|
||||||
</div>
|
</div>
|
||||||
<span class="account-status status-${statusClass}">
|
<span class="account-status status-${statusClass}">
|
||||||
<span class="account-status-dot"></span>
|
<span class="account-status-dot"></span>
|
||||||
@@ -3987,10 +3993,7 @@ function _refreshHosterGroupHeader(name) {
|
|||||||
if (!group) return;
|
if (!group) return;
|
||||||
const accounts = config.hosters[name] || [];
|
const accounts = config.hosters[name] || [];
|
||||||
const summary = _summarizeHosterGroup(accounts);
|
const summary = _summarizeHosterGroup(accounts);
|
||||||
let dot = 'unchecked';
|
const dot = window.AccountStatus.getAccountGroupStatus(summary);
|
||||||
if (summary.error > 0) dot = 'error';
|
|
||||||
else if (summary.checking > 0) dot = 'checking';
|
|
||||||
else if (summary.ok > 0 && summary.unchecked === 0) dot = 'ok';
|
|
||||||
const dotEl = group.querySelector('.account-hoster-group-header .account-status-dot');
|
const dotEl = group.querySelector('.account-hoster-group-header .account-status-dot');
|
||||||
if (dotEl) dotEl.className = `account-status-dot status-${dot}`;
|
if (dotEl) dotEl.className = `account-status-dot status-${dot}`;
|
||||||
const countEl = group.querySelector('.account-hoster-group-count');
|
const countEl = group.querySelector('.account-hoster-group-count');
|
||||||
@@ -4060,16 +4063,17 @@ function renderAccounts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _summarizeHosterGroup(accounts) {
|
function _summarizeHosterGroup(accounts) {
|
||||||
let ok = 0, error = 0, checking = 0, unchecked = 0, disabled = 0;
|
let ok = 0, warn = 0, error = 0, checking = 0, unchecked = 0, disabled = 0;
|
||||||
for (const a of accounts) {
|
for (const a of accounts) {
|
||||||
if (a.enabled === false) { disabled++; continue; }
|
if (a.enabled === false) { disabled++; continue; }
|
||||||
const s = (accountStatuses[a.id] && accountStatuses[a.id].status) || 'unchecked';
|
const s = (accountStatuses[a.id] && accountStatuses[a.id].status) || 'unchecked';
|
||||||
if (s === 'ok' || s === 'warn') ok++;
|
if (s === 'ok') ok++;
|
||||||
|
else if (s === 'warn' || s === 'otp_required') warn++;
|
||||||
else if (s === 'error') error++;
|
else if (s === 'error') error++;
|
||||||
else if (s === 'checking') checking++;
|
else if (s === 'checking') checking++;
|
||||||
else unchecked++;
|
else unchecked++;
|
||||||
}
|
}
|
||||||
return { ok, error, checking, unchecked, disabled, total: accounts.length };
|
return { ok, warn, error, checking, unchecked, disabled, total: accounts.length };
|
||||||
}
|
}
|
||||||
|
|
||||||
function _hosterGroupOpenState(name, summary) {
|
function _hosterGroupOpenState(name, summary) {
|
||||||
@@ -4192,10 +4196,7 @@ function _buildHosterSettingsHtml(name) {
|
|||||||
function _buildAccountHosterGroupHtml(name, accounts) {
|
function _buildAccountHosterGroupHtml(name, accounts) {
|
||||||
const summary = _summarizeHosterGroup(accounts);
|
const summary = _summarizeHosterGroup(accounts);
|
||||||
const isOpen = _hosterGroupOpenState(name, summary);
|
const isOpen = _hosterGroupOpenState(name, summary);
|
||||||
let dot = 'unchecked';
|
const dot = window.AccountStatus.getAccountGroupStatus(summary);
|
||||||
if (summary.error > 0) dot = 'error';
|
|
||||||
else if (summary.checking > 0) dot = 'checking';
|
|
||||||
else if (summary.ok > 0 && summary.unchecked === 0) dot = 'ok';
|
|
||||||
const countLabel = `${summary.ok}/${summary.total}`;
|
const countLabel = `${summary.ok}/${summary.total}`;
|
||||||
const arrow = isOpen ? '▼' : '▶';
|
const arrow = isOpen ? '▼' : '▶';
|
||||||
let cardsHtml = '';
|
let cardsHtml = '';
|
||||||
@@ -4299,6 +4300,7 @@ function bindAccountListeners(container) {
|
|||||||
if (btn.dataset.accountEdit) return openAccountModal(btn.dataset.accountEdit);
|
if (btn.dataset.accountEdit) return openAccountModal(btn.dataset.accountEdit);
|
||||||
if (btn.dataset.accountDelete) return openDeleteAccountModal(btn.dataset.accountDelete);
|
if (btn.dataset.accountDelete) return openDeleteAccountModal(btn.dataset.accountDelete);
|
||||||
if (btn.dataset.accountCheck) return checkSingleAccount(btn.dataset.accountCheck);
|
if (btn.dataset.accountCheck) return checkSingleAccount(btn.dataset.accountCheck);
|
||||||
|
if (btn.dataset.accountOtpSubmit) return submitAccountOtp(btn.dataset.accountOtpSubmit);
|
||||||
if (btn.dataset.accountReactivate) {
|
if (btn.dataset.accountReactivate) {
|
||||||
const accountId = btn.dataset.accountReactivate;
|
const accountId = btn.dataset.accountReactivate;
|
||||||
const hoster = btn.dataset.accountReactivateHoster;
|
const hoster = btn.dataset.accountReactivateHoster;
|
||||||
@@ -4413,6 +4415,46 @@ async function checkSingleAccount(accountId) {
|
|||||||
updateAccountCard(accountId);
|
updateAccountCard(accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function submitAccountOtp(accountId) {
|
||||||
|
if (!accountId || healthCheckRunning) return;
|
||||||
|
const found = findAccountById(accountId);
|
||||||
|
if (!found || found.account.enabled === false) return;
|
||||||
|
const card = Array.from(document.querySelectorAll('.account-card')).find(el => el.dataset.accountId === accountId);
|
||||||
|
const otpInput = card?.querySelector('[data-account-otp-input]');
|
||||||
|
const otp = otpInput?.value.trim() || '';
|
||||||
|
if (otpInput) otpInput.setCustomValidity('');
|
||||||
|
if (!otp) {
|
||||||
|
if (otpInput) {
|
||||||
|
otpInput.setCustomValidity('Bitte den OTP-Code eingeben.');
|
||||||
|
otpInput.reportValidity();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const submitButton = card?.querySelector('[data-account-otp-submit]');
|
||||||
|
healthCheckRunning = true;
|
||||||
|
accountStatuses[accountId] = { status: 'checking', message: 'OTP wird geprüft…' };
|
||||||
|
if (otpInput) otpInput.disabled = true;
|
||||||
|
if (submitButton) {
|
||||||
|
submitButton.disabled = true;
|
||||||
|
submitButton.textContent = 'Prüfe…';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await window.api.runHealthCheck({ hosters: [{ hoster: found.name, accountId, otp }] });
|
||||||
|
const row = result && Array.isArray(result.results)
|
||||||
|
? result.results.find(item => item.accountId === accountId)
|
||||||
|
: null;
|
||||||
|
accountStatuses[accountId] = row
|
||||||
|
? { status: row.status || 'error', message: row.message || '' }
|
||||||
|
: { status: 'error', message: 'Keine Antwort vom Hoster erhalten' };
|
||||||
|
} catch (err) {
|
||||||
|
accountStatuses[accountId] = { status: 'error', message: err.message || 'OTP-Prüfung fehlgeschlagen' };
|
||||||
|
} finally {
|
||||||
|
healthCheckRunning = false;
|
||||||
|
updateAccountCard(accountId);
|
||||||
|
renderHosterModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Per-hoster overrides for the login form. VOE only accepts emails — the
|
// Per-hoster overrides for the login form. VOE only accepts emails — the
|
||||||
// generic "Username / E-Mail" label sent users down a confusing rabbit hole
|
// generic "Username / E-Mail" label sent users down a confusing rabbit hole
|
||||||
// (login fails → upload fetches login redirect → "CSRF token nicht gefunden").
|
// (login fails → upload fetches login redirect → "CSRF token nicht gefunden").
|
||||||
|
|||||||
@@ -425,6 +425,7 @@
|
|||||||
<script src="../lib/throttle-timer.js"></script>
|
<script src="../lib/throttle-timer.js"></script>
|
||||||
<script src="../lib/serialized-runner.js"></script>
|
<script src="../lib/serialized-runner.js"></script>
|
||||||
<script src="account-submit.js"></script>
|
<script src="account-submit.js"></script>
|
||||||
|
<script src="account-status.js"></script>
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1004,6 +1004,8 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
|
|||||||
.account-card-info { flex: 1; min-width: 0; }
|
.account-card-info { flex: 1; min-width: 0; }
|
||||||
.account-card-title { font-size: 14px; font-weight: 600; }
|
.account-card-title { font-size: 14px; font-weight: 600; }
|
||||||
.account-card-subtitle { font-size: 11px; color: var(--text-muted); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.account-card-subtitle { font-size: 11px; color: var(--text-muted); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.account-otp-action { display: flex; align-items: center; gap: 6px; margin-top: 8px; }
|
||||||
|
.account-otp-input { width: 150px; min-height: 28px; padding: 5px 8px; font-size: 11px; }
|
||||||
.account-card-actions { display: flex; gap: 6px; flex-shrink: 0; }
|
.account-card-actions { display: flex; gap: 6px; flex-shrink: 0; }
|
||||||
|
|
||||||
.account-status {
|
.account-status {
|
||||||
@@ -1184,6 +1186,7 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
|
|||||||
.account-hoster-group .account-card:last-child { margin-bottom: 0; }
|
.account-hoster-group .account-card:last-child { margin-bottom: 0; }
|
||||||
.account-status-dot.status-ok { background: #4caf50; }
|
.account-status-dot.status-ok { background: #4caf50; }
|
||||||
.account-status-dot.status-error { background: #e57373; }
|
.account-status-dot.status-error { background: #e57373; }
|
||||||
|
.account-status-dot.status-warn { background: #f0c36c; }
|
||||||
.account-status-dot.status-checking { background: #f0c36c; }
|
.account-status-dot.status-checking { background: #f0c36c; }
|
||||||
.account-status-dot.status-unchecked { background: #6c757d; }
|
.account-status-dot.status-unchecked { background: #6c757d; }
|
||||||
.account-hoster-group-header .account-status-dot {
|
.account-hoster-group-header .account-status-dot {
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const {
|
||||||
|
getAccountGroupStatus,
|
||||||
|
getAccountStatusPresentation
|
||||||
|
} = require('../renderer/account-status');
|
||||||
|
|
||||||
|
test('mixed account results use warning group status', () => {
|
||||||
|
assert.equal(getAccountGroupStatus({ total: 3, disabled: 0, ok: 2, error: 1, checking: 0, unchecked: 0 }), 'warn');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('group is red only when every active account has an error', () => {
|
||||||
|
assert.equal(getAccountGroupStatus({ total: 3, disabled: 0, ok: 0, error: 3, checking: 0, unchecked: 0 }), 'error');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('group is green when every active account is ready', () => {
|
||||||
|
assert.equal(getAccountGroupStatus({ total: 3, disabled: 0, ok: 3, error: 0, checking: 0, unchecked: 0 }), 'ok');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('warning-only accounts keep the group orange', () => {
|
||||||
|
assert.equal(getAccountGroupStatus({ total: 2, disabled: 0, ok: 0, warn: 2, error: 0, checking: 0, unchecked: 0 }), 'warn');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('OTP-required account exposes warning presentation', () => {
|
||||||
|
assert.deepEqual(getAccountStatusPresentation('otp_required'), {
|
||||||
|
statusClass: 'warn',
|
||||||
|
label: 'OTP erforderlich',
|
||||||
|
requiresOtp: true
|
||||||
|
});
|
||||||
|
});
|
||||||
+50
-7
@@ -109,10 +109,10 @@ setTimeout(async () => {
|
|||||||
const addAccountEnabled = await wc.executeJavaScript('document.getElementById("addAccountBtn")?.disabled === false');
|
const addAccountEnabled = await wc.executeJavaScript('document.getElementById("addAccountBtn")?.disabled === false');
|
||||||
check('Add account button enabled', addAccountEnabled);
|
check('Add account button enabled', addAccountEnabled);
|
||||||
|
|
||||||
const emptyAccountAction = await wc.executeJavaScript('document.querySelector("[data-account-empty-add]")?.textContent?.trim()');
|
const emptyAccountAction = await wc.executeJavaScript('document.querySelector("[data-account-empty-add]")?.textContent?.trim() || document.getElementById("addAccountBtn")?.textContent?.trim()');
|
||||||
check('Empty account state offers direct action', emptyAccountAction === 'Ersten Account hinzufügen');
|
check('Account list offers direct add action', emptyAccountAction === 'Ersten Account hinzufügen' || emptyAccountAction === 'Account hinzufügen');
|
||||||
|
|
||||||
await wc.executeJavaScript('document.querySelector("[data-account-empty-add]").focus(); document.querySelector("[data-account-empty-add]").click()');
|
await wc.executeJavaScript('(() => { const trigger = document.querySelector("[data-account-empty-add]") || document.getElementById("addAccountBtn"); trigger?.focus(); trigger?.click(); })()');
|
||||||
await new Promise(r => setTimeout(r, 200));
|
await new Promise(r => setTimeout(r, 200));
|
||||||
|
|
||||||
const accountModalVisible = await wc.executeJavaScript('document.getElementById("accountModal")?.style.display');
|
const accountModalVisible = await wc.executeJavaScript('document.getElementById("accountModal")?.style.display');
|
||||||
@@ -143,7 +143,7 @@ setTimeout(async () => {
|
|||||||
check('5 hosters exist', hosterCount === 5);
|
check('5 hosters exist', hosterCount === 5);
|
||||||
|
|
||||||
const accountSubmitLabel = await wc.executeJavaScript('document.getElementById("saveAccountBtn")?.textContent');
|
const accountSubmitLabel = await wc.executeJavaScript('document.getElementById("saveAccountBtn")?.textContent');
|
||||||
check('Account submit label is Prüfen und anlegen', accountSubmitLabel === 'Prüfen und anlegen');
|
check('Account submit label is Prüfen und speichern', accountSubmitLabel === 'Prüfen und speichern');
|
||||||
|
|
||||||
const credentialInputs = await wc.executeJavaScript('document.querySelectorAll("#accountCredsFields .key-input").length');
|
const credentialInputs = await wc.executeJavaScript('document.querySelectorAll("#accountCredsFields .key-input").length');
|
||||||
check('Credential inputs rendered', credentialInputs === 2);
|
check('Credential inputs rendered', credentialInputs === 2);
|
||||||
@@ -155,12 +155,56 @@ setTimeout(async () => {
|
|||||||
const accountModalHidden = await wc.executeJavaScript('document.getElementById("accountModal")?.style.display');
|
const accountModalHidden = await wc.executeJavaScript('document.getElementById("accountModal")?.style.display');
|
||||||
check('Escape closes account modal', accountModalHidden === 'none');
|
check('Escape closes account modal', accountModalHidden === 'none');
|
||||||
|
|
||||||
const restoredAccountFocus = await wc.executeJavaScript('document.activeElement?.hasAttribute("data-account-empty-add")');
|
const restoredAccountFocus = await wc.executeJavaScript('document.activeElement?.hasAttribute("data-account-empty-add") || document.activeElement?.id === "addAccountBtn"');
|
||||||
check('Account modal restores trigger focus', restoredAccountFocus === true);
|
check('Account modal restores trigger focus', restoredAccountFocus === true);
|
||||||
|
|
||||||
const fallbackAccountFocus = await wc.executeJavaScript('document.querySelector("[data-account-empty-add]").focus(); document.querySelector("[data-account-empty-add]").click(); document.querySelector("[data-account-empty-add]").remove(); document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true })); document.activeElement?.id');
|
const fallbackAccountFocus = await wc.executeJavaScript('(() => { const trigger = document.querySelector("[data-account-empty-add]") || document.getElementById("addAccountBtn"); trigger.focus(); trigger.click(); document.querySelector("[data-account-empty-add]")?.remove(); document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true })); return document.activeElement?.id; })()');
|
||||||
check('Account modal restores stable focus after list rerender', fallbackAccountFocus === 'addAccountBtn');
|
check('Account modal restores stable focus after list rerender', fallbackAccountFocus === 'addAccountBtn');
|
||||||
|
|
||||||
|
const mixedGroupStatus = await wc.executeJavaScript(\`(() => {
|
||||||
|
config.hosters['byse.sx'] = [
|
||||||
|
{ id: 'ui-status-ok-1', enabled: true, authType: 'api', apiKey: 'key-one' },
|
||||||
|
{ id: 'ui-status-ok-2', enabled: true, authType: 'api', apiKey: 'key-two' },
|
||||||
|
{ id: 'ui-status-error', enabled: true, authType: 'api', apiKey: 'key-three' }
|
||||||
|
];
|
||||||
|
accountStatuses['ui-status-ok-1'] = { status: 'ok', message: 'Bereit' };
|
||||||
|
accountStatuses['ui-status-ok-2'] = { status: 'ok', message: 'Bereit' };
|
||||||
|
accountStatuses['ui-status-error'] = { status: 'error', message: 'Deaktiviert durch Administrator' };
|
||||||
|
renderAccounts();
|
||||||
|
const dot = document.querySelector('.account-hoster-group[data-hoster-group="byse.sx"] .account-hoster-group-header .account-status-dot');
|
||||||
|
return [dot?.className, getComputedStyle(dot).backgroundColor].join('|');
|
||||||
|
})()\`);
|
||||||
|
check('Mixed account results use orange group status', mixedGroupStatus === 'account-status-dot status-warn|rgb(240, 195, 108)');
|
||||||
|
|
||||||
|
const allErrorGroupStatus = await wc.executeJavaScript(\`(() => {
|
||||||
|
accountStatuses['ui-status-ok-1'] = { status: 'error', message: 'Fehler' };
|
||||||
|
accountStatuses['ui-status-ok-2'] = { status: 'error', message: 'Fehler' };
|
||||||
|
renderAccounts();
|
||||||
|
return document.querySelector('.account-hoster-group[data-hoster-group="byse.sx"] .account-hoster-group-header .account-status-dot')?.className;
|
||||||
|
})()\`);
|
||||||
|
check('All-error account group uses red status', allErrorGroupStatus === 'account-status-dot status-error');
|
||||||
|
|
||||||
|
const otpCardState = await wc.executeJavaScript(\`(() => {
|
||||||
|
config.hosters['doodstream.com'] = [{ id: 'ui-status-otp', enabled: true, authType: 'login', username: 'otp@example.com', password: 'password' }];
|
||||||
|
accountStatuses['ui-status-otp'] = { status: 'otp_required', message: 'OTP wurde an deine E-Mail gesendet.' };
|
||||||
|
renderAccounts();
|
||||||
|
const card = [...document.querySelectorAll('.account-card')].find(el => el.dataset.accountId === 'ui-status-otp');
|
||||||
|
return [
|
||||||
|
card?.querySelector('[data-account-otp-input]')?.getAttribute('placeholder'),
|
||||||
|
card?.querySelector('[data-account-otp-submit]')?.textContent?.trim(),
|
||||||
|
card?.querySelector('.account-status')?.textContent?.trim()
|
||||||
|
].join('|');
|
||||||
|
})()\`);
|
||||||
|
check('OTP-required account exposes inline code input and save action', otpCardState === 'Code aus E-Mail|Prüfen und speichern|OTP erforderlich');
|
||||||
|
|
||||||
|
const otpSubmitState = await wc.executeJavaScript(\`(() => {
|
||||||
|
const card = [...document.querySelectorAll('.account-card')].find(el => el.dataset.accountId === 'ui-status-otp');
|
||||||
|
card.querySelector('[data-account-otp-input]').value = '123456';
|
||||||
|
card.querySelector('[data-account-otp-submit]').click();
|
||||||
|
return [accountStatuses['ui-status-otp']?.status, String(card.querySelector('[data-account-otp-submit]')?.disabled)].join('|');
|
||||||
|
})()\`);
|
||||||
|
check('Inline OTP submission starts validation', otpSubmitState === 'checking|true');
|
||||||
|
|
||||||
console.log('\\n=== Settings View ===');
|
console.log('\\n=== Settings View ===');
|
||||||
|
|
||||||
await wc.executeJavaScript('document.querySelector(".tab[data-view=\\'settings\\']").click()');
|
await wc.executeJavaScript('document.querySelector(".tab[data-view=\\'settings\\']").click()');
|
||||||
@@ -248,7 +292,6 @@ try {
|
|||||||
const electronPath = path.join(__dirname, '..', 'node_modules', '.bin', 'electron');
|
const electronPath = path.join(__dirname, '..', 'node_modules', '.bin', 'electron');
|
||||||
const mainPath = path.join(__dirname, '..', 'main.js');
|
const mainPath = path.join(__dirname, '..', 'main.js');
|
||||||
|
|
||||||
// We'll use --require to inject the test after the main process loads
|
|
||||||
const result = execSync(
|
const result = execSync(
|
||||||
`"${electronPath}" --require "${injectPath}" "${mainPath}"`,
|
`"${electronPath}" --require "${injectPath}" "${mainPath}"`,
|
||||||
{ cwd: path.join(__dirname, '..'), timeout: 20000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
{ cwd: path.join(__dirname, '..'), timeout: 20000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ const {
|
|||||||
} = require('../renderer/account-submit');
|
} = require('../renderer/account-submit');
|
||||||
|
|
||||||
test('account submit labels stay exact for add, edit, and OTP retries', () => {
|
test('account submit labels stay exact for add, edit, and OTP retries', () => {
|
||||||
assert.equal(getAccountSubmitLabel({ isEdit: false, hasOtp: false }), 'Prüfen und anlegen');
|
assert.equal(getAccountSubmitLabel({ isEdit: false, hasOtp: false }), 'Prüfen und speichern');
|
||||||
assert.equal(getAccountSubmitLabel({ isEdit: true, hasOtp: false }), 'Prüfen und speichern');
|
assert.equal(getAccountSubmitLabel({ isEdit: true, hasOtp: false }), 'Prüfen und speichern');
|
||||||
assert.equal(getAccountSubmitLabel({ isEdit: false, hasOtp: true }), 'Prüfen und anlegen');
|
assert.equal(getAccountSubmitLabel({ isEdit: false, hasOtp: true }), 'Prüfen und speichern');
|
||||||
assert.equal(getAccountSubmitLabel({ isEdit: true, hasOtp: true }), 'Prüfen und speichern');
|
assert.equal(getAccountSubmitLabel({ isEdit: true, hasOtp: true }), 'Prüfen und speichern');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user