fix: serialize managed backup actions

Queue copy and delete operations by sanitized backup ID so confirmations survive renderer refreshes and deletes follow in-flight copies without silent cancellation. Reload the managed list exactly once after successful create and delete mutations, and invalidate stale UI state when the authoritative refresh fails.
This commit is contained in:
Sucukdeluxe
2026-08-22 11:42:29 +02:00
parent f741edaf2f
commit f2c6630866
2 changed files with 196 additions and 71 deletions
+83 -46
View File
@@ -63,8 +63,9 @@ let hosterSettings = {};
let uploading = false; let uploading = false;
let healthCheckRunning = false; let healthCheckRunning = false;
let managedOnlineBackups = []; let managedOnlineBackups = [];
let managedOnlineBackupsAuthoritative = false;
let managedOnlineBackupLoadGeneration = 0; let managedOnlineBackupLoadGeneration = 0;
const managedOnlineBackupPendingActions = new Map(); const managedOnlineBackupOperationQueues = new Map();
let _rLongTasks = 0, _rLongTaskMax = 0, _rFrameLast = 0, _rFrameWorst = 0, _rFrameCount = 0, _rFrameJank = 0, _rPerfLastLog = 0, _rPerfWindowStart = 0; let _rLongTasks = 0, _rLongTaskMax = 0, _rFrameLast = 0, _rFrameWorst = 0, _rFrameCount = 0, _rFrameJank = 0, _rPerfLastLog = 0, _rPerfWindowStart = 0;
function _rElLabel(el) { function _rElLabel(el) {
@@ -2667,6 +2668,13 @@ function normalizeManagedOnlineBackups(entries) {
function replaceManagedOnlineBackups(entries) { function replaceManagedOnlineBackups(entries) {
managedOnlineBackups = normalizeManagedOnlineBackups(entries); managedOnlineBackups = normalizeManagedOnlineBackups(entries);
managedOnlineBackupsAuthoritative = true;
renderManagedOnlineBackups();
}
function invalidateManagedOnlineBackups() {
managedOnlineBackups = [];
managedOnlineBackupsAuthoritative = false;
renderManagedOnlineBackups(); renderManagedOnlineBackups();
} }
@@ -2674,6 +2682,10 @@ function renderManagedOnlineBackups() {
const list = document.getElementById('managedOnlineBackupList'); const list = document.getElementById('managedOnlineBackupList');
if (!list) return; if (!list) return;
const content = document.createDocumentFragment(); const content = document.createDocumentFragment();
if (!managedOnlineBackupsAuthoritative) {
list.replaceChildren(content);
return;
}
if (managedOnlineBackups.length === 0) { if (managedOnlineBackups.length === 0) {
const empty = document.createElement('p'); const empty = document.createElement('p');
empty.className = 'online-backup-managed-empty'; empty.className = 'online-backup-managed-empty';
@@ -2699,11 +2711,11 @@ function renderManagedOnlineBackups() {
deleteButton.type = 'button'; deleteButton.type = 'button';
deleteButton.className = 'btn btn-danger online-backup-delete-btn'; deleteButton.className = 'btn btn-danger online-backup-delete-btn';
deleteButton.textContent = localizeUiText('Online-Backup löschen'); deleteButton.textContent = localizeUiText('Online-Backup löschen');
const pendingAction = managedOnlineBackupPendingActions.get(entry.id); const pending = managedOnlineBackupOperationQueues.has(entry.id);
copyButton.disabled = pendingAction === 'copy' || pendingAction === 'delete'; copyButton.disabled = pending;
deleteButton.disabled = pendingAction === 'delete'; deleteButton.disabled = pending;
copyButton.addEventListener('click', () => copyManagedOnlineBackup(entry, copyButton)); copyButton.addEventListener('click', () => copyManagedOnlineBackup(entry));
deleteButton.addEventListener('click', () => deleteManagedOnlineBackup(entry, row)); deleteButton.addEventListener('click', () => deleteManagedOnlineBackup(entry));
actions.append(copyButton, deleteButton); actions.append(copyButton, deleteButton);
row.append(key, created, actions); row.append(key, created, actions);
content.append(row); content.append(row);
@@ -2718,63 +2730,90 @@ async function loadManagedOnlineBackups() {
const result = await window.api.listManagedOnlineBackups(); const result = await window.api.listManagedOnlineBackups();
if (generation !== managedOnlineBackupLoadGeneration) return false; if (generation !== managedOnlineBackupLoadGeneration) return false;
if (!result?.ok || !Array.isArray(result.entries)) { if (!result?.ok || !Array.isArray(result.entries)) {
invalidateManagedOnlineBackups();
setOnlineBackupStatus('Online-Sicherungen konnten nicht geladen werden', 'error'); setOnlineBackupStatus('Online-Sicherungen konnten nicht geladen werden', 'error');
return false; return false;
} }
replaceManagedOnlineBackups(result.entries); replaceManagedOnlineBackups(result.entries);
setOnlineBackupStatus('', '');
return true; return true;
} catch { } catch {
if (generation === managedOnlineBackupLoadGeneration) setOnlineBackupStatus('Online-Sicherungen konnten nicht geladen werden', 'error'); if (generation === managedOnlineBackupLoadGeneration) {
invalidateManagedOnlineBackups();
setOnlineBackupStatus('Online-Sicherungen konnten nicht geladen werden', 'error');
}
return false; return false;
} }
} }
async function copyManagedOnlineBackup(entry, button) { function hasManagedOnlineBackup(id) {
if (button.disabled || managedOnlineBackupPendingActions.has(entry.id)) return; return managedOnlineBackups.some(entry => entry.id === id);
managedOnlineBackupPendingActions.set(entry.id, 'copy');
button.disabled = true;
try {
const result = await window.api.copyManagedOnlineBackup(entry.id);
if (!result?.ok) {
setOnlineBackupStatus('Online-Sicherung konnte nicht kopiert werden', 'error');
return;
}
showCopyToast('Online-Schlüssel kopiert');
} catch {
setOnlineBackupStatus('Online-Sicherung konnte nicht kopiert werden', 'error');
} finally {
managedOnlineBackupPendingActions.delete(entry.id);
renderManagedOnlineBackups();
}
} }
async function deleteManagedOnlineBackup(entry, row) { function enqueueManagedOnlineBackupOperation(id, action, operation) {
const previous = managedOnlineBackupOperationQueues.get(id)?.promise || Promise.resolve();
const promise = previous.catch(() => {}).then(operation);
const state = { action, promise };
managedOnlineBackupOperationQueues.set(id, state);
renderManagedOnlineBackups();
return promise.finally(() => {
if (managedOnlineBackupOperationQueues.get(id) !== state) return;
managedOnlineBackupOperationQueues.delete(id);
renderManagedOnlineBackups();
});
}
async function copyManagedOnlineBackup(entry) {
const id = entry.id;
if (!hasManagedOnlineBackup(id) || managedOnlineBackupOperationQueues.has(id)) return;
await enqueueManagedOnlineBackupOperation(id, 'copy', async () => {
try {
const result = await window.api.copyManagedOnlineBackup(id);
if (!result?.ok) {
setOnlineBackupStatus('Online-Sicherung konnte nicht kopiert werden', 'error');
return;
}
showCopyToast('Online-Schlüssel kopiert');
} catch {
setOnlineBackupStatus('Online-Sicherung konnte nicht kopiert werden', 'error');
}
});
}
async function deleteManagedOnlineBackup(entry) {
const id = entry.id;
const confirmed = await showAppConfirm({ const confirmed = await showAppConfirm({
title: 'Online-Backup löschen', title: 'Online-Backup löschen',
message: 'Dieses verschlüsselte Online-Backup wird dauerhaft vom Server gelöscht.', message: 'Dieses verschlüsselte Online-Backup wird dauerhaft vom Server gelöscht.',
confirmText: 'Löschen', confirmText: 'Löschen',
danger: true danger: true
}); });
if (!confirmed || !row.isConnected) return; if (!confirmed) return;
if (managedOnlineBackupPendingActions.has(entry.id)) return; if (!hasManagedOnlineBackup(id)) {
managedOnlineBackupPendingActions.set(entry.id, 'delete'); setOnlineBackupStatus('Online-Sicherung konnte nicht gelöscht werden', 'error');
const controls = [...row.querySelectorAll('button')]; return;
controls.forEach(button => { button.disabled = true; }); }
try { if (managedOnlineBackupOperationQueues.get(id)?.action === 'delete') {
const result = await window.api.deleteManagedOnlineBackup(entry.id); setOnlineBackupStatus('Online-Sicherung konnte nicht gelöscht werden', 'error');
if (!result?.ok || result.removedId !== entry.id) { return;
}
await enqueueManagedOnlineBackupOperation(id, 'delete', async () => {
if (!hasManagedOnlineBackup(id)) {
setOnlineBackupStatus('Online-Sicherung konnte nicht gelöscht werden', 'error'); setOnlineBackupStatus('Online-Sicherung konnte nicht gelöscht werden', 'error');
return; return;
} }
managedOnlineBackupLoadGeneration++; try {
replaceManagedOnlineBackups(managedOnlineBackups.filter(item => item.id !== result.removedId)); const result = await window.api.deleteManagedOnlineBackup(id);
setOnlineBackupStatus('Schlüssel gelöscht', 'success'); if (!result?.ok || result.removedId !== id) {
} catch { setOnlineBackupStatus('Online-Sicherung konnte nicht gelöscht werden', 'error');
setOnlineBackupStatus('Online-Sicherung konnte nicht gelöscht werden', 'error'); return;
} finally { }
managedOnlineBackupPendingActions.delete(entry.id); if (!await loadManagedOnlineBackups()) return;
renderManagedOnlineBackups(); setOnlineBackupStatus('Schlüssel gelöscht', 'success');
} } catch {
setOnlineBackupStatus('Online-Sicherung konnte nicht gelöscht werden', 'error');
}
});
} }
async function doOnlineBackupCreate() { async function doOnlineBackupCreate() {
@@ -2783,7 +2822,6 @@ async function doOnlineBackupCreate() {
try { try {
await flushPendingSettingsSaves(); await flushPendingSettingsSaves();
openOnlineBackupView(); openOnlineBackupView();
await loadManagedOnlineBackups();
} catch { } catch {
openOnlineBackupView(); openOnlineBackupView();
setOnlineBackupStatus('Nicht alle Einstellungen konnten gespeichert werden', 'error'); setOnlineBackupStatus('Nicht alle Einstellungen konnten gespeichert werden', 'error');
@@ -2795,12 +2833,11 @@ async function doOnlineBackupCreate() {
setOnlineBackupStatus('Verschlüssele und speichere Einstellungen…', 'busy'); setOnlineBackupStatus('Verschlüssele und speichere Einstellungen…', 'busy');
try { try {
const result = await window.api.createManagedOnlineBackup(); const result = await window.api.createManagedOnlineBackup();
if (!result?.ok || normalizeManagedOnlineBackups([result.entry]).length !== 1) { if (!result?.ok) {
setOnlineBackupStatus('Online-Sicherung konnte nicht erstellt werden', 'error'); setOnlineBackupStatus('Online-Sicherung konnte nicht erstellt werden', 'error');
return; return;
} }
managedOnlineBackupLoadGeneration++; if (!await loadManagedOnlineBackups()) return;
replaceManagedOnlineBackups([...managedOnlineBackups, result.entry]);
setOnlineBackupStatus('Neuer Schlüssel erstellt. Ältere Schlüssel bleiben gültig.', 'success'); setOnlineBackupStatus('Neuer Schlüssel erstellt. Ältere Schlüssel bleiben gültig.', 'success');
showCopyToast('Online-Schlüssel erstellt'); showCopyToast('Online-Schlüssel erstellt');
} catch { } catch {
+113 -25
View File
@@ -63,6 +63,25 @@ test('Windows compositor paints the full hidden surface with an RDP session envi
fs.writeFileSync(preloadPath, ` fs.writeFileSync(preloadPath, `
const { contextBridge } = require('electron'); const { contextBridge } = require('electron');
const managedOnlineBackupProbeCalls = []; const managedOnlineBackupProbeCalls = [];
const managedOnlineBackupListResponses = [
{ ok: true, entries: [
{ id: 'AAAAAAAAAAAAAAAAAAAAAA', displayKey: 'MHU2-ABCD…1234', createdAt: '2026-08-20T08:00:00.000Z' },
{ id: 'BBBBBBBBBBBBBBBBBBBBBB', displayKey: 'MHU2-ZYXW…9876', createdAt: '2026-08-22T10:00:00.000Z' }
] },
{ ok: true, entries: [
{ id: 'DDDDDDDDDDDDDDDDDDDDDD', displayKey: 'MHU2-DFGH…2468', createdAt: '2026-08-25T10:00:00.000Z' },
{ id: 'AAAAAAAAAAAAAAAAAAAAAA', displayKey: 'MHU2-ABCD…1234', createdAt: '2026-08-20T08:00:00.000Z' }
] },
{ ok: true, entries: [
{ id: 'CCCCCCCCCCCCCCCCCCCCCC', displayKey: 'MHU2-QWER…4321', createdAt: '2026-08-23T12:00:00.000Z' },
{ id: 'EEEEEEEEEEEEEEEEEEEEEE', displayKey: 'MHU2-ETYU…1357', createdAt: '2026-08-26T10:00:00.000Z' },
{ id: 'DDDDDDDDDDDDDDDDDDDDDD', displayKey: 'MHU2-DFGH…2468', createdAt: '2026-08-25T10:00:00.000Z' }
] },
{ ok: false, error: 'Online-Sicherungen konnten nicht geladen werden' }
];
let managedOnlineBackupListIndex = 0;
let managedOnlineBackupCreateIndex = 0;
let pendingManagedOnlineBackupCopy = null;
let pendingManagedOnlineBackupDelete = null; let pendingManagedOnlineBackupDelete = null;
contextBridge.exposeInMainWorld('api', { contextBridge.exposeInMainWorld('api', {
onUpdateAvailable() {}, onUpdateAvailable() {},
@@ -70,19 +89,26 @@ contextBridge.exposeInMainWorld('api', {
onPrepareClose() {}, onPrepareClose() {},
getConfig() { return new Promise(() => {}); }, getConfig() { return new Promise(() => {}); },
listManagedOnlineBackups() { listManagedOnlineBackups() {
managedOnlineBackupProbeCalls.push(['list']); managedOnlineBackupListIndex++;
return Promise.resolve({ ok: true, entries: [ managedOnlineBackupProbeCalls.push(['list', managedOnlineBackupListIndex]);
{ id: 'AAAAAAAAAAAAAAAAAAAAAA', displayKey: 'MHU2-ABCD…1234', createdAt: '2026-08-20T08:00:00.000Z' }, return Promise.resolve(managedOnlineBackupListResponses[managedOnlineBackupListIndex - 1]);
{ id: 'BBBBBBBBBBBBBBBBBBBBBB', displayKey: 'MHU2-ZYXW…9876', createdAt: '2026-08-22T10:00:00.000Z' }
] });
}, },
createManagedOnlineBackup() { createManagedOnlineBackup() {
managedOnlineBackupProbeCalls.push(['create']); managedOnlineBackupCreateIndex++;
return Promise.resolve({ ok: true, entry: { id: 'CCCCCCCCCCCCCCCCCCCCCC', displayKey: 'MHU2-QWER…4321', createdAt: '2026-08-23T12:00:00.000Z' } }); managedOnlineBackupProbeCalls.push(['create', managedOnlineBackupCreateIndex]);
const entry = managedOnlineBackupCreateIndex === 1
? { id: 'CCCCCCCCCCCCCCCCCCCCCC', displayKey: 'MHU2-QWER…4321', createdAt: '2026-08-23T12:00:00.000Z' }
: { id: 'FFFFFFFFFFFFFFFFFFFFFF', displayKey: 'MHU2-FGHJ…8642', createdAt: '2026-08-27T12:00:00.000Z' };
return Promise.resolve({ ok: true, entry });
}, },
copyManagedOnlineBackup(id) { copyManagedOnlineBackup(id) {
managedOnlineBackupProbeCalls.push(['copy', id]); managedOnlineBackupProbeCalls.push(['copy', id]);
return Promise.resolve({ ok: true }); return new Promise(resolve => { pendingManagedOnlineBackupCopy = resolve; });
},
releaseManagedOnlineBackupCopy() {
const resolve = pendingManagedOnlineBackupCopy;
pendingManagedOnlineBackupCopy = null;
resolve({ ok: true });
}, },
deleteManagedOnlineBackup(id) { deleteManagedOnlineBackup(id) {
managedOnlineBackupProbeCalls.push(['delete', id]); managedOnlineBackupProbeCalls.push(['delete', id]);
@@ -100,8 +126,19 @@ contextBridge.exposeInMainWorld('api', {
const fixture = document.createElement('section'); const fixture = document.createElement('section');
fixture.innerHTML = '<div id="managedOnlineBackupList"></div><div id="onlineBackupStatus"></div><button id="createOnlineBackupBtn"></button><input id="onlineBackupKeyInput"><button id="restoreOnlineBackupBtn"></button>'; fixture.innerHTML = '<div id="managedOnlineBackupList"></div><div id="onlineBackupStatus"></div><button id="createOnlineBackupBtn"></button><input id="onlineBackupKeyInput"><button id="restoreOnlineBackupBtn"></button>';
document.body.append(fixture); document.body.append(fixture);
const waitFor = async predicate => {
for (let attempt = 0; attempt < 80; attempt++) {
if (await predicate()) return true;
await new Promise(resolve => setTimeout(resolve, 0));
}
return false;
};
let confirmation = null; let confirmation = null;
showAppConfirm = async options => { confirmation = options; return true; }; let resolveConfirmation = null;
showAppConfirm = options => {
confirmation = options;
return new Promise(resolve => { resolveConfirmation = resolve; });
};
flushPendingSettingsSaves = async () => {}; flushPendingSettingsSaves = async () => {};
openOnlineBackupView = () => {}; openOnlineBackupView = () => {};
await loadManagedOnlineBackups(); await loadManagedOnlineBackups();
@@ -109,26 +146,59 @@ contextBridge.exposeInMainWorld('api', {
const maliciousEntry = { id: 'DDDDDDDDDDDDDDDDDDDDDD', displayKey: 'prefix MHU2-' + 'X'.repeat(70) + ' suffix', createdAt: '2026-08-24T12:00:00.000Z' }; const maliciousEntry = { id: 'DDDDDDDDDDDDDDDDDDDDDD', displayKey: 'prefix MHU2-' + 'X'.repeat(70) + ' suffix', createdAt: '2026-08-24T12:00:00.000Z' };
replaceManagedOnlineBackups([...managedOnlineBackups, maliciousEntry]); replaceManagedOnlineBackups([...managedOnlineBackups, maliciousEntry]);
const secretRejected = !document.body.textContent.includes(maliciousEntry.displayKey) && managedOnlineBackups.every(entry => entry.displayKey !== maliciousEntry.displayKey); const secretRejected = !document.body.textContent.includes(maliciousEntry.displayKey) && managedOnlineBackups.every(entry => entry.displayKey !== maliciousEntry.displayKey);
document.querySelector('.online-backup-copy-btn').click(); const originalRow = document.querySelector('.online-backup-managed-row');
await new Promise(resolve => setTimeout(resolve, 0)); originalRow.querySelector('.online-backup-delete-btn').click();
document.querySelector('.online-backup-delete-btn').click(); await waitFor(() => typeof resolveConfirmation === 'function');
await new Promise(resolve => setTimeout(resolve, 0));
renderManagedOnlineBackups(); renderManagedOnlineBackups();
const pendingDeleteControlsDisabled = [...document.querySelector('.online-backup-managed-row').querySelectorAll('button')].every(button => button.disabled); const rowReplacedDuringConfirmation = !originalRow.isConnected;
window.api.releaseManagedOnlineBackupDelete(); document.querySelector('.online-backup-copy-btn').click();
await waitFor(async () => (await window.api.getManagedOnlineBackupProbeCalls()).some(call => call[0] === 'copy'));
const copyPendingControlsDisabled = [...document.querySelector('.online-backup-managed-row').querySelectorAll('button')].every(button => button.disabled);
resolveConfirmation(true);
await new Promise(resolve => setTimeout(resolve, 0)); await new Promise(resolve => setTimeout(resolve, 0));
const deleteStartedBeforeCopyFinished = (await window.api.getManagedOnlineBackupProbeCalls()).some(call => call[0] === 'delete');
window.api.releaseManagedOnlineBackupCopy();
const deleteStartedAfterCopy = await waitFor(async () => (await window.api.getManagedOnlineBackupProbeCalls()).filter(call => call[0] === 'delete').length === 1);
const deletePendingControlsDisabled = deleteStartedAfterCopy && [...document.querySelector('.online-backup-managed-row').querySelectorAll('button')].every(button => button.disabled);
if (deleteStartedAfterCopy) window.api.releaseManagedOnlineBackupDelete();
await waitFor(async () => (await window.api.getManagedOnlineBackupProbeCalls()).filter(call => call[0] === 'list').length === 2);
const afterDelete = [...document.querySelectorAll('.online-backup-managed-key')].map(element => element.textContent); const afterDelete = [...document.querySelectorAll('.online-backup-managed-key')].map(element => element.textContent);
const deletePostRefreshCount = (await window.api.getManagedOnlineBackupProbeCalls()).filter(call => call[0] === 'list').length - 1;
const listCallsBeforeCreate = (await window.api.getManagedOnlineBackupProbeCalls()).filter(call => call[0] === 'list').length;
await doOnlineBackupCreate(); await doOnlineBackupCreate();
const afterCreate = [...document.querySelectorAll('.online-backup-managed-key')].map(element => element.textContent); const afterCreate = [...document.querySelectorAll('.online-backup-managed-key')].map(element => element.textContent);
const authoritativeStateFields = [...new Set(managedOnlineBackups.flatMap(entry => Object.keys(entry)))].sort();
const callsAfterCreate = await window.api.getManagedOnlineBackupProbeCalls();
const createPostRefreshCount = callsAfterCreate.filter(call => call[0] === 'list').length - listCallsBeforeCreate;
const listCallsBeforeFailedCreate = callsAfterCreate.filter(call => call[0] === 'list').length;
await doOnlineBackupCreate();
const failedCreatePostRefreshCount = (await window.api.getManagedOnlineBackupProbeCalls()).filter(call => call[0] === 'list').length - listCallsBeforeFailedCreate;
const refreshFailure = {
rowCount: document.querySelectorAll('.online-backup-managed-row').length,
stateCount: managedOnlineBackups.length,
emptyStateVisible: Boolean(document.querySelector('.online-backup-managed-empty')),
status: document.getElementById('onlineBackupStatus').textContent,
statusState: document.getElementById('onlineBackupStatus').dataset.state
};
const calls = await window.api.getManagedOnlineBackupProbeCalls();
return { return {
initialKeys, initialKeys,
secretRejected, secretRejected,
pendingDeleteControlsDisabled, rowReplacedDuringConfirmation,
copyPendingControlsDisabled,
deleteStartedBeforeCopyFinished,
deleteStartedAfterCopy,
deletePendingControlsDisabled,
deleteCallCount: calls.filter(call => call[0] === 'delete').length,
afterDelete, afterDelete,
deletePostRefreshCount,
afterCreate, afterCreate,
authoritativeStateFields,
createPostRefreshCount,
failedCreatePostRefreshCount,
refreshFailure,
confirmation, confirmation,
calls: await window.api.getManagedOnlineBackupProbeCalls(), calls,
stateFields: Object.keys(managedOnlineBackups[0] || {}).sort(),
secretInBody: /MHU2-[A-Za-z0-9_-]{70}/.test(document.body.textContent) secretInBody: /MHU2-[A-Za-z0-9_-]{70}/.test(document.body.textContent)
}; };
})()`; })()`;
@@ -268,9 +338,25 @@ app.whenReady().then(async () => {
assert.equal(result.liveSpeedChart.baselinePresent, false); assert.equal(result.liveSpeedChart.baselinePresent, false);
assert.deepEqual(result.onlineBackupBehavior.initialKeys, ['MHU2-ZYXW…9876', 'MHU2-ABCD…1234']); assert.deepEqual(result.onlineBackupBehavior.initialKeys, ['MHU2-ZYXW…9876', 'MHU2-ABCD…1234']);
assert.equal(result.onlineBackupBehavior.secretRejected, true); assert.equal(result.onlineBackupBehavior.secretRejected, true);
assert.equal(result.onlineBackupBehavior.pendingDeleteControlsDisabled, true); assert.equal(result.onlineBackupBehavior.rowReplacedDuringConfirmation, true);
assert.deepEqual(result.onlineBackupBehavior.afterDelete, ['MHU2-ABCD…1234']); assert.equal(result.onlineBackupBehavior.copyPendingControlsDisabled, true);
assert.equal(result.onlineBackupBehavior.afterCreate[0], 'MHU2-QWER…4321'); assert.equal(result.onlineBackupBehavior.deleteStartedBeforeCopyFinished, false);
assert.equal(result.onlineBackupBehavior.deleteStartedAfterCopy, true);
assert.equal(result.onlineBackupBehavior.deletePendingControlsDisabled, true);
assert.equal(result.onlineBackupBehavior.deleteCallCount, 1);
assert.deepEqual(result.onlineBackupBehavior.afterDelete, ['MHU2-DFGH…2468', 'MHU2-ABCD…1234']);
assert.equal(result.onlineBackupBehavior.deletePostRefreshCount, 1);
assert.deepEqual(result.onlineBackupBehavior.afterCreate, ['MHU2-ETYU…1357', 'MHU2-DFGH…2468', 'MHU2-QWER…4321']);
assert.deepEqual(result.onlineBackupBehavior.authoritativeStateFields, ['createdAt', 'displayKey', 'id']);
assert.equal(result.onlineBackupBehavior.createPostRefreshCount, 1);
assert.equal(result.onlineBackupBehavior.failedCreatePostRefreshCount, 1);
assert.deepEqual(result.onlineBackupBehavior.refreshFailure, {
rowCount: 0,
stateCount: 0,
emptyStateVisible: false,
status: 'Online backups could not be loaded',
statusState: 'error'
});
assert.equal(result.onlineBackupBehavior.afterCreate.length, 3); assert.equal(result.onlineBackupBehavior.afterCreate.length, 3);
assert.deepEqual(result.onlineBackupBehavior.confirmation, { assert.deepEqual(result.onlineBackupBehavior.confirmation, {
title: 'Online-Backup löschen', title: 'Online-Backup löschen',
@@ -279,13 +365,15 @@ app.whenReady().then(async () => {
danger: true danger: true
}); });
assert.deepEqual(result.onlineBackupBehavior.calls, [ assert.deepEqual(result.onlineBackupBehavior.calls, [
['list'], ['list', 1],
['copy', 'BBBBBBBBBBBBBBBBBBBBBB'], ['copy', 'BBBBBBBBBBBBBBBBBBBBBB'],
['delete', 'BBBBBBBBBBBBBBBBBBBBBB'], ['delete', 'BBBBBBBBBBBBBBBBBBBBBB'],
['list'], ['list', 2],
['create'] ['create', 1],
['list', 3],
['create', 2],
['list', 4]
]); ]);
assert.deepEqual(result.onlineBackupBehavior.stateFields, ['createdAt', 'displayKey', 'id']);
assert.equal(result.onlineBackupBehavior.secretInBody, false); assert.equal(result.onlineBackupBehavior.secretInBody, false);
assert.ok(Math.abs(result.onlineBackupLayout.german.createRight - result.onlineBackupLayout.german.contentRight) <= 1); assert.ok(Math.abs(result.onlineBackupLayout.german.createRight - result.onlineBackupLayout.german.contentRight) <= 1);
assert.ok(Math.abs(result.onlineBackupLayout.english.createRight - result.onlineBackupLayout.english.contentRight) <= 1); assert.ok(Math.abs(result.onlineBackupLayout.english.createRight - result.onlineBackupLayout.english.contentRight) <= 1);