Files
LEDMatrix/test/js
ChuckandClaude Opus 5.5 7ab6fb1aff refactor(web): read plugins through a PluginCatalog; only the display runs them (#688)
The web process built its own PluginManager and loaded plugins into itself:
store installs and updates loaded or reloaded a web-side copy, and config
saves and enable/disable called on_config_change, on_enable and on_disable
on it. None of that reached the panel, and /plugins/installed reported
runtime state from those copies.

- Add PluginCatalog (src/plugin_system/plugin_catalog.py): manifests,
  directories, display modes, installed version, schema and config reads,
  with no way to run a plugin. app.py and both blueprints use it; the
  plugin_manager blueprint attribute is gone.
- Remove every lifecycle call from the web routes. Config changes already
  reach the display through ConfigService (on_config_change) and the
  enabled-set reconcile.
- Health and metrics readers move to api_v3.health_tracker /
  resource_monitor. /plugins/installed reports loaded/state/error_info as
  null (the display does not publish them) and enabled by the display's
  rule.
- Store install, update and uninstall answer restart_required when the
  running display will not pick the change up by itself
  (display_restart_required). The restart banner follows the flag via
  window.noteRestartRequired instead of the /config/main URL heuristic;
  /config/main now sends restart_required: true.
- The one remaining in-process import of plugin code (Starlark helper
  modules, oauth_flow action scripts) goes through
  _import_plugin_code_in_web_process() until a web-entry contract.
- /plugins/installed reports vegas_participation (from #682) from the
  user's setting or the manifest, with vegas_participation_source; when
  only the plugin's code decides it, null with source 'runtime', since the
  web process no longer has plugin instances to ask.
- Check & Update All keeps its restart flags when the final list refresh
  fails, and asks for a restart when an enabled plugin's first request got
  no answer and the re-sent one found it up to date.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:39:44 -04:00
..

Web-interface JS tests

Covers web_interface/static/v3/js/plugins/list_filter.js (the shared search/filter/sort controller) and the plugin-manager grids that use it: Installed Plugins, the Plugin Store, and Starlark Apps.

There is no JS toolchain in this repo, so these are plain node scripts with no test framework. Each prints ok/FAIL lines and exits non-zero on failure.

Running

cd test/js
npm install                 # jsdom, for the DOM suites only
node run_all.js

The unit suites need nothing but node; test/test_js_unit_suites.py runs every unit/*.js under pytest, so CI covers them. The DOM suites additionally need a running web interface, because they test against the real server-rendered HTML and the real API rather than fixtures:

# in another shell, from the repo root
EMULATOR=true python3 web_interface/app.py         # http://localhost:5000

# or point the suites at a device
BASE=http://<pi-ip>:5000 node run_all.js

run_all.js skips the DOM suites (rather than failing) when jsdom is missing or nothing is listening, so it stays useful in a bare checkout. REQUIRE_DOM=1 makes that a failure instead.

CI runs everything: the Web UI JS tests job in .github/workflows/test.yml installs jsdom, starts the web interface in emulator mode on port 5000 and runs run_all.js with REQUIRE_DOM=1. The DOM suites don't assume a particular device: the store suite checks pagination whichever side of 48 plugins the live registry is, and the Tools suite supplies two sample Starlark apps when the server has none.

The suites

Suite Needs a server Covers
unit/test_list_filter.js no ListFilter search/filter/sort/count/sticky, and the installed-plugins config extracted verbatim from plugins_manager.js so the test can't drift from it
unit/test_update_all.js no PluginInstallManager.updateAll from plugins/install_manager.js: Check & Update All sends only plugin ids (never starlark: app entries), re-sends a request that got no HTTP answer (web service restarting) instead of skipping that plugin, never re-sends one that got any HTTP answer (the real api_client.js classifies a proxy 502 or a JSON error without error_code as API_ERROR), and counts a no-op update as already up to date in the summary. Also run by test/web_interface/test_update_all_plugins.py so CI covers it
unit/test_render_cards.js no renderInstalledCards markup, both empty states, and HTML-escaping of hostile plugin metadata
unit/test_style_editor_element_keys.js no elementKeys()/styleRows()/positionRows() from widgets/style-editor.js: every customization.layout entry gets exactly one row -- paired with its style element through core's x-layout-key (so score belongs to score_text, not a second row), or a position row of its own, leaves included -- since the widget claims the whole layout block from the generic fallback renderer
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
dom/test_no_double_fetch.js yes Loads the whole plugins_manager.js and counts requests: typing in the store search must filter the cached list, not refetch /api/v3/plugins/store/list
dom/test_tools_sections.js yes The Tools tab's MQTT bridge and Pixlet editor sections: form prefill, the write-only password (blank means unchanged), the running-session banner and countdown, and that the editor link points at the host you loaded the page from

Point the DOM suites at a rig with a full plugin set when it matters — a dev box with two plugins installed will pass while exercising very little.

Notes for whoever changes this next

  • The suites read the shipped files off disk and, for the DOM ones, the partial from the running server. They do not keep their own copy of the markup, so renaming an element id will fail them loudly rather than silently pass.
  • unit/test_list_filter.js evals a slice of plugins_manager.js located by the text function installedSortName(plugin). If that function is renamed, fix the slice markers rather than pasting a copy of the config into the test.
  • A few assertions exist specifically to stop earlier bugs coming back: trailing spaces surviving the search debounce; a multi-word query that spans two adjacent search fields (field order in the haystack is load-bearing); window.installedPlugins staying at full length while the grid is filtered.
  • Watch for assertions that can pass vacuously. Several here deliberately guard against it — e.g. counting only non-skeleton cards, and asserting a search phrase matches something before comparing two results.

The old-vs-new differential suites used to verify that the store and Starlark migrations were behaviour-preserving are not included: they compared against the pre-refactor implementation, which now only exists in git history. See PR #540 if that comparison ever needs redoing.