mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 06:15:09 +00:00
* fix(web-ui): Update All sends the live installed list and redraws the grid updateAll() preferred PluginStateManager.installedPlugins over window.installedPlugins. Only updateAll's own end-of-run refresh ever fills PluginStateManager, so from the second run on it sent the first run's plugins: one uninstalled since failed with "plugin not found" and one installed since was never updated. That refresh also only replaced window.installedPlugins, so the installed cards and the Updates badge kept offering "Update to vX" for what had just been updated. Read window.installedPlugins, the list plugins_manager.js republishes after every install, uninstall and refresh, keeping PluginStateManager as the fallback for a page without it, and refresh through pluginManager.loadInstalledPlugins(true), which redraws the grid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-ui): list each plugin's display modes in /plugins/installed The on-demand modal fills its Display Mode select from plugin.display_modes, but /plugins/installed never sent the field. Every plugin offered one option, its own id, under "This plugin exposes a single display mode"; the display resolved that id to the plugin's first mode, so a multi-mode plugin could only be started, or pinned, there. Add display_modes to each entry, read from the plugin catalog (get_plugin_display_modes), the same declared list /display/modes and on-demand/start use, keeping only strings. Single-mode plugins still get one option and the same hint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-ui): enable a store install by its installed id, and not on reinstall The store's Install button enabled the new plugin by the registry id it installed. Weather, Music, Stocks and Leaderboard install under the id their manifests declare (weather -> ledmatrix-weather); the plugin list, the config section and /plugins/toggle know only that id, so the toggle answered 404 "Plugin not found" and the plugin stayed disabled behind "installed, but enabling it failed". The same button on an installed plugin (Reinstall) enabled it too, switching a plugin the user had turned off back on. POST /plugins/install now names the installed plugin: plugin_id in the direct answer and in the queued operation's result, read from the installed manifest found the way the store's update and uninstall find it (_find_plugin_path: id, aliases, plugin_path name), else the requested id. The client reloads the list, then enables that id; from an answer without it, the installed entry the store entry matches (findInstalledStorePlugin, which isStorePluginInstalled now uses). A reinstall, decided by the same match that labelled the button, reloads the list and leaves the enabled state alone. test/js/plugins_manager_sandbox.js runs the whole of plugins_manager.js in a vm context against a fake DOM and API, for suites that drive its real flows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-ui): wait for long store installs; on timeout reload, not fail pollOperationStatus gave a queued install 60 polls, a second apart, then reported "Install operation timed out" as an error and stopped. The server allows the plugin's dependency install 300 s on its own (install_requirements_file in store_install.py), after a download that fetches the plugin a file at a time, so installs that went on to succeed were reported as failed, never enabled, and left out of the installed list until the page was reloaded. Give installs INSTALL_POLL_MAX_ATTEMPTS (600, ten minutes). When even that runs out, reload the installed list and the store badges and warn that the install may still be running; nothing is enabled without the operation's answer. Uninstall keeps the default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-ui): build the store's category filter from the store's plugins The #plugin-category select listed seven fixed categories while the registry uses about twenty (productivity, utility, transit, finance, ...), so roughly a third of the store could not be filtered to, and "Financial" missed the plugin filed under "finance". The template now ships only "All Categories"; syncStoreCategoryOptions, run by applyStoreFiltersAndSort, adds one option per category the cached store plugins have (case folded, as the filter compares), keeps the current choice, and rebuilds only when the set changes or the partial was swapped in afresh -- the way the Starlark section builds its own. The test sandbox gains window.addEventListener (initPluginsPage needs it) and quiets the script's "element not found" warnings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-ui): one handler for the GitHub-URL Install button #install-plugin-from-url had an inline onclick calling window.handleGitHubPluginInstall, and attachInstallButtonHandler also gave it a click listener that installs, so both ran on every click (and on Enter, which clicks it). The inline handler threw a ReferenceError -- it called isGithubUrl, which is local to the plugin-manager IIFE, from outside it -- so only the listener's request went out; correcting that scope alone would have sent every install twice. Remove the inline onclick and the window.handleGitHubPluginInstall it called, which nothing else uses. The listener, which already sent the only request, is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
364 lines
19 KiB
JavaScript
364 lines
19 KiB
JavaScript
// "Check & Update All" -- which ids it sends to POST /api/v3/plugins/update.
|
|
//
|
|
// Pins two bugs seen on a real device (core 3.4.0):
|
|
//
|
|
// 1. /plugins/installed lists installed Starlark apps as virtual
|
|
// `starlark:<app_id>` entries. Update-all sent those to the plugin
|
|
// updater, which answered 500 "plugin not found" for each one.
|
|
//
|
|
// 2. A web-service restart landed mid-run. The request in flight died with
|
|
// the old process and the next one (stock-news) was refused while the
|
|
// service was coming back; both were recorded as failures and never sent
|
|
// again, so stock-news -- installed, disabled, with an update waiting --
|
|
// was silently not updated.
|
|
//
|
|
// Runs the shipped install_manager.js (it exports itself under node) against
|
|
// a fake PluginAPI. The installed list is the device's, trimmed.
|
|
|
|
const path = require('path');
|
|
const V3 = path.resolve(__dirname, '../../../web_interface/static/v3');
|
|
|
|
let pass = 0, fail = 0;
|
|
const ok = (label, cond, extra) => cond
|
|
? (pass++, console.log(' ok ' + label))
|
|
: (fail++, console.log(' FAIL ' + label + (extra !== undefined ? ' ' + JSON.stringify(extra) : '')));
|
|
|
|
global.window = {};
|
|
const Manager = require(path.join(V3, 'js/plugins/install_manager.js'));
|
|
|
|
const INSTALLED = [
|
|
{ id: 'ledmatrix-flights', enabled: true, version: '1.14.0' },
|
|
{ id: 'stock-news', enabled: false, version: '2.6.2', latest_version: '2.8.0', update_available: true },
|
|
{ id: 'static-image', enabled: false, version: '1.1.3' },
|
|
{ id: 'starlark-apps', enabled: false, version: '1.0.0' }, // a real plugin -- keep it
|
|
{ id: 'pomodoro-timer', enabled: false, version: '1.3.6' },
|
|
{ id: 'starlark:analogtime', enabled: false, version: 'starlark', is_starlark_app: true },
|
|
{ id: 'starlark:analogclock', enabled: true, version: 'starlark', is_starlark_app: true },
|
|
];
|
|
const EXPECTED = ['ledmatrix-flights', 'stock-news', 'static-image', 'starlark-apps', 'pomodoro-timer'];
|
|
|
|
const netErr = () => ({ error_code: 'NETWORK_ERROR', message: 'Failed to fetch' });
|
|
|
|
function fakeApi(behaviour = {}) {
|
|
const calls = [];
|
|
return {
|
|
calls,
|
|
updatePlugin: async (id) => {
|
|
calls.push(id);
|
|
const b = behaviour[id];
|
|
if (typeof b === 'function') return b(calls.filter(c => c === id).length);
|
|
return { status: 'success', message: `Plugin ${id} updated successfully` };
|
|
},
|
|
};
|
|
}
|
|
|
|
function setup(api, { stateList, windowList, pluginManager } = {}) {
|
|
global.window = {
|
|
PluginAPI: api,
|
|
installedPlugins: windowList,
|
|
PluginStateManager: stateList === undefined ? undefined : {
|
|
installedPlugins: stateList,
|
|
loadInstalledPlugins: async () => stateList,
|
|
},
|
|
pluginManager,
|
|
};
|
|
}
|
|
|
|
const noSleep = { sleep: async () => {} };
|
|
|
|
(async () => {
|
|
console.log('\nselection');
|
|
ok('starlark app entries are not updatable',
|
|
!Manager.isUpdatablePlugin(INSTALLED[5]) && !Manager.isUpdatablePlugin(INSTALLED[6]));
|
|
ok('a starlark: id without the flag is still excluded',
|
|
!Manager.isUpdatablePlugin({ id: 'starlark:foo' }));
|
|
ok('the starlark-apps plugin itself is updatable', Manager.isUpdatablePlugin(INSTALLED[3]));
|
|
ok('a disabled plugin with an update is updatable', Manager.isUpdatablePlugin(INSTALLED[1]));
|
|
ok('entries without a usable id are skipped',
|
|
!Manager.isUpdatablePlugin({}) && !Manager.isUpdatablePlugin(null) && !Manager.isUpdatablePlugin({ id: '' }));
|
|
const sel = Manager.updatablePlugins(INSTALLED).map(p => p.id);
|
|
ok('updatablePlugins keeps list order and drops only starlark apps',
|
|
JSON.stringify(sel) === JSON.stringify(EXPECTED), sel);
|
|
|
|
console.log('\nupdateAll sends only plugin ids');
|
|
{
|
|
const api = fakeApi();
|
|
setup(api, { windowList: INSTALLED });
|
|
const progress = [];
|
|
const results = await Manager.updateAll((i, n, id) => progress.push([i, n, id]), noSleep);
|
|
ok('POSTs exactly the non-starlark ids, in order',
|
|
JSON.stringify(api.calls) === JSON.stringify(EXPECTED), api.calls);
|
|
ok('no starlark: id reached the plugin updater', !api.calls.some(id => id.startsWith('starlark:')));
|
|
ok('one result per plugin sent', results.length === EXPECTED.length, results.length);
|
|
ok('progress total counts only what is sent',
|
|
progress.length === EXPECTED.length && progress.every(([, n]) => n === EXPECTED.length), progress);
|
|
}
|
|
{
|
|
// A page without the plugin manager has no window.installedPlugins.
|
|
const api = fakeApi();
|
|
setup(api, { stateList: INSTALLED });
|
|
await Manager.updateAll(null, noSleep);
|
|
ok('the PluginStateManager list (no live list) is filtered the same way',
|
|
JSON.stringify(api.calls) === JSON.stringify(EXPECTED), api.calls);
|
|
}
|
|
|
|
console.log('\na second run sends the live list, not the first run\'s snapshot');
|
|
{
|
|
// Run 1 leaves PluginStateManager holding a, b, c. Then c is uninstalled
|
|
// and d installed: plugins_manager.js publishes that only as
|
|
// window.installedPlugins. Run 2 used to send a, b, c -- c failed as
|
|
// "plugin not found" and d, which had an update waiting, was skipped.
|
|
const api = fakeApi({
|
|
c: () => { throw { error_code: 'PLUGIN_UPDATE_FAILED', message: 'Plugin update failed: plugin not found' }; },
|
|
});
|
|
const stale = [{ id: 'a' }, { id: 'b' }, { id: 'c' }];
|
|
setup(api, { stateList: stale, windowList: [{ id: 'a' }, { id: 'b' }, { id: 'd' }] });
|
|
const results = await Manager.updateAll(null, noSleep);
|
|
ok('sends exactly what is installed now',
|
|
JSON.stringify(api.calls) === JSON.stringify(['a', 'b', 'd']), api.calls);
|
|
ok('...so nothing fails over an uninstalled plugin', results.every(r => r.success), results);
|
|
}
|
|
{
|
|
const api = fakeApi();
|
|
setup(api, { stateList: INSTALLED, windowList: [] });
|
|
const results = await Manager.updateAll(null, noSleep);
|
|
ok('an empty live list means nothing is installed: nothing is sent',
|
|
api.calls.length === 0 && results.length === 0, api.calls);
|
|
}
|
|
|
|
console.log('\nthe end-of-run refresh redraws the installed grid');
|
|
{
|
|
// PluginStateManager's refresh replaced window.installedPlugins and
|
|
// nothing else: the cards kept "Update to vX" and the Updates badge
|
|
// kept its count. The plugin manager's load renders the grid.
|
|
const loads = [];
|
|
let stateLoads = 0;
|
|
const pluginManager = { loadInstalledPlugins: async (force) => { loads.push(force); } };
|
|
setup(fakeApi(), { stateList: INSTALLED, windowList: INSTALLED, pluginManager });
|
|
window.PluginStateManager.loadInstalledPlugins = async () => { stateLoads++; };
|
|
await Manager.updateAll(null, noSleep);
|
|
ok('reloads through the plugin manager once, forced past its caches',
|
|
JSON.stringify(loads) === JSON.stringify([true]), loads);
|
|
ok('...instead of PluginStateManager', stateLoads === 0, stateLoads);
|
|
}
|
|
{
|
|
const pluginManager = { loadInstalledPlugins: async () => { throw new Error('offline'); } };
|
|
const answer = { status: 'success', data: { update_status: 'updated' }, restart_required: true };
|
|
setup(fakeApi({ 'ledmatrix-flights': () => answer }), { windowList: INSTALLED, pluginManager });
|
|
const warn = console.warn;
|
|
console.warn = () => {};
|
|
let results;
|
|
try {
|
|
results = await Manager.updateAll(null, noSleep);
|
|
} finally {
|
|
console.warn = warn;
|
|
}
|
|
ok('a failed plugin-manager reload still returns the results with their restart flag',
|
|
Array.isArray(results) && Manager.restartRequest(results) === answer);
|
|
}
|
|
{
|
|
const api = fakeApi();
|
|
setup(api, { windowList: INSTALLED.filter(p => p.is_starlark_app) });
|
|
const results = await Manager.updateAll(null, noSleep);
|
|
ok('a list of only starlark apps sends nothing', api.calls.length === 0 && results.length === 0);
|
|
}
|
|
|
|
console.log('\nweb service restarting mid-run (the stock-news case)');
|
|
{
|
|
// ledmatrix-flights is in flight when the service stops (reset); stock-news
|
|
// is refused twice while it comes back; then everything answers.
|
|
const api = fakeApi({
|
|
'ledmatrix-flights': (n) => { if (n === 1) throw netErr(); return { status: 'success', message: 'ok' }; },
|
|
'stock-news': (n) => { if (n <= 2) throw netErr(); return { status: 'success', message: 'Plugin stock-news updated successfully' }; },
|
|
});
|
|
setup(api, { windowList: INSTALLED });
|
|
const slept = [];
|
|
const results = await Manager.updateAll(null, { sleep: async ms => { slept.push(ms); }, retryDelaysMs: [5, 10, 20] });
|
|
const byId = Object.fromEntries(results.map(r => [r.pluginId, r]));
|
|
ok('stock-news is sent again until the server answers',
|
|
api.calls.filter(id => id === 'stock-news').length === 3, api.calls);
|
|
ok('stock-news ends up updated, not skipped', byId['stock-news'] && byId['stock-news'].success === true, byId['stock-news']);
|
|
ok('the request lost with the old process is re-sent too',
|
|
byId['ledmatrix-flights'] && byId['ledmatrix-flights'].success === true);
|
|
ok('it backs off between attempts', JSON.stringify(slept) === JSON.stringify([5, 5, 10]), slept);
|
|
ok('every plugin still gets exactly one result',
|
|
JSON.stringify(results.map(r => r.pluginId)) === JSON.stringify(EXPECTED), results.map(r => r.pluginId));
|
|
}
|
|
{
|
|
const api = fakeApi({ 'stock-news': () => { throw netErr(); } });
|
|
setup(api, { windowList: INSTALLED });
|
|
const results = await Manager.updateAll(null, { sleep: async () => {}, retryDelaysMs: [1, 1] });
|
|
const r = results.find(x => x.pluginId === 'stock-news');
|
|
ok('a server that never comes back is retried a bounded number of times',
|
|
api.calls.filter(id => id === 'stock-news').length === 3, api.calls);
|
|
ok('...then reported as a failure, and the run continues',
|
|
r && r.success === false && api.calls[api.calls.length - 1] === 'pomodoro-timer');
|
|
}
|
|
{
|
|
const api = fakeApi({ 'static-image': () => { throw { error_code: 'PLUGIN_UPDATE_FAILED', message: 'check logs' }; } });
|
|
setup(api, { windowList: INSTALLED });
|
|
const results = await Manager.updateAll(null, { sleep: async () => { throw new Error('must not sleep'); } });
|
|
ok('an HTTP error answer is not retried',
|
|
api.calls.filter(id => id === 'static-image').length === 1, api.calls);
|
|
ok('...and is reported as a failure', results.find(x => x.pluginId === 'static-image').success === false);
|
|
}
|
|
|
|
console.log('\nthe real api_client.js: only a missing HTTP answer is retried');
|
|
{
|
|
// The fake PluginAPI above decides error_code itself. This runs the shipped
|
|
// client so its classification is what gets tested: a proxy's 502 page or
|
|
// a JSON 500 without error_code used to come back as NETWORK_ERROR and be
|
|
// re-sent five more times.
|
|
const PluginAPI = require(path.join(V3, 'js/plugins/api_client.js'));
|
|
const run = async (makeFetch) => {
|
|
let requests = 0, sleeps = 0;
|
|
global.fetch = async () => { requests++; return makeFetch(requests); };
|
|
global.window = { PluginAPI, installedPlugins: [{ id: 'stock-news' }] };
|
|
const results = await Manager.updateAll(null, { sleep: async () => { sleeps++; }, retryDelaysMs: [1, 1, 1] });
|
|
return { requests, sleeps, result: results[0] };
|
|
};
|
|
const httpAnswer = (status, json) => ({ ok: status < 400, status, json });
|
|
|
|
let r = await run(() => httpAnswer(502, async () => { throw new SyntaxError('Unexpected token <'); }));
|
|
ok('a 502 with an HTML body is sent once', r.requests === 1 && r.sleeps === 0, r);
|
|
ok('...and is an API_ERROR carrying the status, not NETWORK_ERROR',
|
|
r.result.success === false && r.result.error.error_code === 'API_ERROR' && r.result.error.status === 502, r.result.error);
|
|
|
|
r = await run(() => httpAnswer(500, async () => ({ status: 'error', message: 'boom' })));
|
|
ok('a JSON 500 without error_code is sent once', r.requests === 1 && r.sleeps === 0, r);
|
|
ok('...and keeps the server message', r.result.error.message === 'boom', r.result.error);
|
|
|
|
r = await run(() => httpAnswer(500, async () => ({ status: 'error', error_code: 'PLUGIN_UPDATE_FAILED', message: 'x' })));
|
|
ok('a structured error is passed through unchanged',
|
|
r.requests === 1 && r.result.error.error_code === 'PLUGIN_UPDATE_FAILED', r.result.error);
|
|
|
|
r = await run((n) => {
|
|
if (n === 1) throw new TypeError('Failed to fetch');
|
|
return httpAnswer(200, async () => ({ status: 'success', message: 'ok', data: { update_status: 'updated' } }));
|
|
});
|
|
ok('a fetch() that rejects is NETWORK_ERROR and re-sent', r.requests === 2 && r.sleeps === 1 && r.result.success, r);
|
|
|
|
r = await run(() => httpAnswer(200, async () => { throw new SyntaxError('Unexpected end of JSON input'); }));
|
|
ok('an unreadable 200 is not retried either', r.requests === 1 && r.result.error.error_code === 'API_ERROR', r);
|
|
|
|
// Every endpoint is one of the client's own API paths; one that could
|
|
// leave baseURL never reaches fetch(), and plugin ids are encoded.
|
|
const urls = [];
|
|
global.fetch = async (url) => { urls.push(url); return httpAnswer(200, async () => ({ status: 'success' })); };
|
|
const refusal = async (endpoint) => {
|
|
try { await PluginAPI.request(endpoint, 'POST'); return null; } catch (e) { return e.error_code; }
|
|
};
|
|
const bad = ['//evil.example/x', '/plugins/../../x', '/a\b', '/a b', 'plugins', null];
|
|
const codes = [];
|
|
for (const endpoint of bad) codes.push(await refusal(endpoint));
|
|
ok('an endpoint that could leave the API path is refused before fetch()',
|
|
codes.every(c => c === 'INVALID_ENDPOINT') && urls.length === 0, { codes, urls });
|
|
await PluginAPI.resetPluginConfig('a/../b&x=1');
|
|
ok('a plugin id is encoded into the URL, not spliced into it',
|
|
urls[0] === '/api/v3/plugins/config/reset?plugin_id=a%2F..%2Fb%26x%3D1', urls);
|
|
delete global.fetch;
|
|
}
|
|
|
|
console.log('\nsummary: a no-op update is not counted as updated');
|
|
{
|
|
const answer = (update_status, message) => ({ success: true, result: { status: 'success', message, data: { update_status } } });
|
|
const results = [
|
|
answer('updated', 'Plugin a updated to version 2.8.0'),
|
|
// ZIP-installed monorepo plugin already at the registry version: the
|
|
// route used to call this "updated successfully".
|
|
answer('up_to_date', 'Plugin stock-news already up to date (version 2.8.0)'),
|
|
answer('up_to_date', 'Plugin clock already up to date (commit abcdef1)'),
|
|
answer('local_only', 'Plugin mine is managed locally and does not receive registry updates'),
|
|
{ success: false, error: { error_code: 'PLUGIN_UPDATE_FAILED' } },
|
|
];
|
|
const s = Manager.summarizeUpdateResults(results);
|
|
ok('counts come from update_status',
|
|
s.updated === 1 && s.upToDate === 2 && s.localOnly === 1 && s.failed === 1, s);
|
|
ok('toast text names each outcome',
|
|
s.text === '1 updated, 2 already up to date, 1 managed locally, 1 failed', s.text);
|
|
ok('a failure alongside an update is a warning', s.type === 'warning', s.type);
|
|
ok('an older server that only says so in the message is still up to date',
|
|
Manager.updateOutcome({ success: true, result: { message: 'Plugin x already up to date (commit 1234567)' } }) === 'up_to_date');
|
|
ok('a success without a status or telltale message counts as updated',
|
|
Manager.updateOutcome({ success: true, result: { message: 'Plugin x updated successfully' } }) === 'updated');
|
|
const allNoop = Manager.summarizeUpdateResults([answer('up_to_date', ''), answer('up_to_date', '')]);
|
|
ok('nothing to do is a success toast with no "updated"',
|
|
allNoop.type === 'success' && allNoop.text === '2 already up to date', allNoop);
|
|
}
|
|
|
|
console.log('\nrestart banner: driven by the server\'s restart_required');
|
|
{
|
|
const body = (restart_required, restart_message) => ({
|
|
success: true,
|
|
result: { status: 'success', data: { update_status: 'updated' }, restart_required, restart_message },
|
|
});
|
|
const needed = body(true, 'Plugin updated — restart the display to run the new version');
|
|
ok('an update the display is running asks for the banner, with its wording',
|
|
Manager.restartRequest([body(false), needed, body(true, 'second')]) === needed.result);
|
|
ok('updates the display does not run need no restart',
|
|
Manager.restartRequest([body(false), body(false)]) === null);
|
|
ok('a failed request never raises the banner',
|
|
Manager.restartRequest([{ success: false, error: { restart_required: true } }]) === null);
|
|
ok('an older server that sends no flag raises nothing',
|
|
Manager.restartRequest([{ success: true, result: { status: 'success' } }]) === null);
|
|
ok('no results, no banner', Manager.restartRequest(undefined) === null);
|
|
|
|
// The first request's answer was lost; the re-sent one finds nothing to do.
|
|
const lost = (enabled, update_status = 'up_to_date') => ({
|
|
pluginId: 'clock', success: true, afterLostAnswer: true, enabled,
|
|
result: { status: 'success', data: { update_status }, restart_required: false },
|
|
});
|
|
const maybe = Manager.restartRequest([body(false), lost(true)]);
|
|
ok('an enabled plugin up to date after a lost answer may have been updated: banner',
|
|
maybe && maybe.restart_required === true && /clock/.test(maybe.restart_message), maybe);
|
|
ok('...but an explicit answer still wins, with its wording',
|
|
Manager.restartRequest([lost(true), needed]) === needed.result);
|
|
ok('a disabled one needs no restart (enabling it loads it)',
|
|
Manager.restartRequest([lost(false)]) === null);
|
|
ok('nor does one that was not retried',
|
|
Manager.restartRequest([{ ...lost(true), afterLostAnswer: undefined }]) === null);
|
|
}
|
|
|
|
console.log('\nupdateAll keeps what the banner needs');
|
|
{
|
|
// ledmatrix-flights (enabled) loses its first answer, then is up to date.
|
|
const api = fakeApi({
|
|
'ledmatrix-flights': (n) => {
|
|
if (n === 1) throw netErr();
|
|
return { status: 'success', data: { update_status: 'up_to_date' }, restart_required: false };
|
|
},
|
|
});
|
|
setup(api, { windowList: INSTALLED });
|
|
const results = await Manager.updateAll(null, noSleep);
|
|
const flights = results.find(r => r.pluginId === 'ledmatrix-flights');
|
|
ok('a retried entry is marked, with the plugin\'s enabled flag',
|
|
flights.afterLostAnswer === true && flights.enabled === true, flights);
|
|
ok('an entry answered first time is not marked',
|
|
results.filter(r => r.afterLostAnswer).length === 1, results);
|
|
ok('...so the run asks for a restart', Manager.restartRequest(results) !== null);
|
|
}
|
|
{
|
|
const answer = { status: 'success', data: { update_status: 'updated' }, restart_required: true };
|
|
const api = fakeApi({ 'ledmatrix-flights': () => answer });
|
|
setup(api, { stateList: INSTALLED });
|
|
window.PluginStateManager.loadInstalledPlugins = async () => { throw new Error('refresh failed'); };
|
|
const warn = console.warn;
|
|
console.warn = () => {};
|
|
let results;
|
|
try {
|
|
results = await Manager.updateAll(null, noSleep);
|
|
} catch (e) {
|
|
results = e;
|
|
} finally {
|
|
console.warn = warn;
|
|
}
|
|
ok('a failed list refresh still returns the results',
|
|
Array.isArray(results) && results.length === EXPECTED.length, String(results));
|
|
ok('...with the restart flag intact',
|
|
Array.isArray(results) && Manager.restartRequest(results) === answer);
|
|
}
|
|
|
|
console.log(`\n${pass} passed, ${fail} failed`);
|
|
process.exit(fail ? 1 : 0);
|
|
})().catch(e => { console.error(e); process.exit(1); });
|