mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 14:25:08 +00:00
feat(store): read ledmatrix_min_version, aliases and commit from the registry (#686)
The store reads three optional registry fields: ledmatrix_min_version (an incompatible install/update is refused before any download, with a "Needs LEDMatrix X+" card badge), aliases (update/uninstall/reinstall by registry id find a plugin installed under its manifest id, with registry proof only), and commit (shown and linked on the store card). An older plugins.json behaves as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,7 @@ server has none.
|
||||
| `unit/test_style_editor_layout_leaf_columns.js` | no | `columnsFor()` from `widgets/style-editor.js`: a layout-only key whose own value is a leaf (no x/y sub-object, e.g. a `show_logo` toggle) gets a self-keyed column instead of a blank, uneditable row |
|
||||
| `unit/test_style_editor_layout_leaf_collision.js` | no | `columnsFor()` from `widgets/style-editor.js`: a layout-only leaf key still gets its own column even when its name collides with an unrelated element's style sub-field or another layout axis's sub-field |
|
||||
| `unit/test_inline_handler_escaping.js` | no | The store, saved-repository and custom-registry inline `onclick` handlers and the live `window.updateImageList` from `plugins_manager.js`: a registry id, URL or uploaded file name carrying `'`, `"` or entities adds no attributes and reaches the handler intact, and the store's View button opens only http(s) links |
|
||||
| `unit/test_store_registry_fields.js` | no | The store card's registry fields from `plugins_manager.js`: the commit that introduced the listed version (a hex SHA only, linked to that tree), the "Needs LEDMatrix X+" warning, a card from an older registry without either, and `isStorePluginInstalled` answering to `aliases` |
|
||||
| `unit/test_plugin_action_delegation.js` | no | The document-level card-action delegation and `handlePluginAction` from `plugins_manager.js`, run with the handler inside an IIFE as in the real file: each action is handled once, a Starlark app uninstall goes to `DELETE /starlark/apps/<id>`, and an uninstall is confirmed once |
|
||||
| `dom/test_installed_dom.js` | yes | The toolbar in a real DOM: pill/search/sort interaction, the HTMX partial re-swap, and a `getComputedStyle` check that `.filter-pill[data-active]` really matches the emitted markup |
|
||||
| `dom/test_store_dom.js` | yes | Store pagination, per-page, category, tri-state Installed button, and persistence across a re-boot, against the live registry |
|
||||
|
||||
+2
-1
@@ -21,7 +21,8 @@ const UNIT = ['unit/test_list_filter.js', 'unit/test_render_cards.js',
|
||||
'unit/test_style_editor_layout_leaf_columns.js',
|
||||
'unit/test_style_editor_layout_leaf_collision.js',
|
||||
'unit/test_update_all.js', 'unit/test_inline_handler_escaping.js',
|
||||
'unit/test_plugin_action_delegation.js', 'unit/test_file_upload_widget.js'];
|
||||
'unit/test_plugin_action_delegation.js', 'unit/test_file_upload_widget.js',
|
||||
'unit/test_store_registry_fields.js'];
|
||||
const DOM = ['dom/test_installed_dom.js', 'dom/test_store_dom.js', 'dom/test_no_double_fetch.js',
|
||||
'dom/test_tools_sections.js'];
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// The store card shows the registry fields added after 3.7.0 -- the commit
|
||||
// that introduced the listed version and a warning when the plugin needs a
|
||||
// newer core -- and still renders a card from an older registry that has
|
||||
// neither. isStorePluginInstalled also answers to an entry's `aliases`.
|
||||
//
|
||||
// Rendered with the shipped functions (extracted from plugins_manager.js).
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const SRC = fs.readFileSync(
|
||||
path.resolve(__dirname, '../../../web_interface/static/v3/plugins_manager.js'), 'utf8');
|
||||
|
||||
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).slice(0, 400) : '')));
|
||||
|
||||
function extract(opener) {
|
||||
const start = SRC.indexOf(opener);
|
||||
if (start < 0) { console.error('FAIL: cannot find ' + JSON.stringify(opener)); process.exit(1); }
|
||||
let depth = 0;
|
||||
for (let j = SRC.indexOf('{', start); j < SRC.length; j++) {
|
||||
if (SRC[j] === '{') depth++;
|
||||
else if (SRC[j] === '}' && --depth === 0) return SRC.slice(start, j + 1);
|
||||
}
|
||||
console.error('FAIL: unbalanced braces after ' + opener); process.exit(1);
|
||||
}
|
||||
|
||||
class FakeEl {
|
||||
constructor() { this.innerHTML = ''; this.value = ''; this.textContent = ''; }
|
||||
}
|
||||
class TextEl {
|
||||
set textContent(v) { this._t = String(v == null ? '' : v); }
|
||||
get innerHTML() {
|
||||
return (this._t || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
}
|
||||
const els = {};
|
||||
global.document = {
|
||||
getElementById: id => (els[id] ||= new FakeEl()),
|
||||
createElement: () => new TextEl(),
|
||||
};
|
||||
global.window = global;
|
||||
require('../led_escape').install(window);
|
||||
global.pluginLog = () => {};
|
||||
global.isNewPlugin = () => false;
|
||||
global.formatDate = () => '';
|
||||
global.setGridHtmlIfChanged = (container, html) => { container.innerHTML = html; };
|
||||
global.installedPlugins = [];
|
||||
|
||||
// eslint-disable-next-line no-eval
|
||||
eval([
|
||||
'function escapeHtml(text) {', 'function escapeAttribute(text) {', 'function jsStringAttr(value) {',
|
||||
'function isStorePluginInstalled(pluginIdOrPlugin) {', 'function renderPluginStore(plugins) {',
|
||||
].map(extract).join('\n') + '\nglobal.renderPluginStore = renderPluginStore;'
|
||||
+ '\nglobal.isStorePluginInstalled = isStorePluginInstalled;');
|
||||
|
||||
function render(plugin) {
|
||||
renderPluginStore([plugin]);
|
||||
return els['plugin-store-grid'].innerHTML;
|
||||
}
|
||||
|
||||
const SHA = '843588025a81197056f8d96779ccb2be19337ab8';
|
||||
const base = {
|
||||
id: 'weather', name: 'Weather', author: 'ChuckBuilds', category: 'weather',
|
||||
description: 'Forecasts', version: '2.1.0',
|
||||
repo: 'https://github.com/ChuckBuilds/ledmatrix-plugins', plugin_path: 'plugins/ledmatrix-weather',
|
||||
};
|
||||
|
||||
console.log('\n1. a registry with the new fields');
|
||||
let html = render({ ...base, commit: SHA, ledmatrix_min_version: '3.7.0', aliases: ['ledmatrix-weather'] });
|
||||
ok('shows the short commit', html.includes('>8435880<'), html.match(/v2\.1\.0[^\n]*/));
|
||||
ok('links it to the plugin at that commit',
|
||||
html.includes(`href="https://github.com/ChuckBuilds/ledmatrix-plugins/tree/${SHA}/plugins/ledmatrix-weather"`));
|
||||
ok('no compatibility warning when the core is new enough', !html.includes('Needs LEDMatrix'));
|
||||
|
||||
console.log('\n2. a plugin this core cannot run');
|
||||
html = render({ ...base, ledmatrix_min_version: '9.0.0',
|
||||
incompatible_reason: 'Weather requires LEDMatrix 9.0.0 or newer' });
|
||||
ok('warns with the floor', html.includes('Needs LEDMatrix 9.0.0+'));
|
||||
ok('and the reason as its title', html.includes('title="Weather requires LEDMatrix 9.0.0 or newer"'));
|
||||
|
||||
console.log('\n3. an older registry: no commit, floor or aliases');
|
||||
html = render({ ...base });
|
||||
ok('still renders the card and its version', html.includes('class="plugin-card"') && html.includes('v2.1.0'));
|
||||
ok('shows no commit and no warning', !html.includes('font-mono') && !html.includes('Needs LEDMatrix'));
|
||||
|
||||
console.log('\n4. a commit value that is not a SHA is not rendered');
|
||||
html = render({ ...base, commit: 'javascript:alert(1)' });
|
||||
ok('dropped', !html.includes('javascript:') && !html.includes('font-mono'));
|
||||
html = render({ ...base, commit: SHA, repo: 'javascript:alert(1)' });
|
||||
ok('without a web repo link it is plain text, not a link',
|
||||
html.includes('>8435880<') && !html.includes('/tree/'));
|
||||
|
||||
console.log('\n5. installed under an alias');
|
||||
global.installedPlugins = [{ id: 'ledmatrix-weather' }];
|
||||
ok('aliases count as installed',
|
||||
isStorePluginInstalled({ id: 'weather', plugin_path: '', aliases: ['ledmatrix-weather'] }));
|
||||
ok('plugin_path still does (older registry)',
|
||||
isStorePluginInstalled({ id: 'weather', plugin_path: 'plugins/ledmatrix-weather' }));
|
||||
ok('a different plugin is not installed', !isStorePluginInstalled({ id: 'stocks', plugin_path: '' }));
|
||||
|
||||
console.log(`\n${pass} passed, ${fail} failed`);
|
||||
process.exit(fail ? 1 : 0);
|
||||
Reference in New Issue
Block a user