Files
LEDMatrix/test/js/README.md
T
ChuckandClaude Opus 5.5 2b34f2545c fix(web): keep disabled plugins in the saved rotation order and Vegas exclusions
PluginOrderList draws one row per enabled plugin and, once drawn, rewrites
its hidden inputs (plugin_rotation_order, vegas_plugin_order,
vegas_excluded_plugins) from those rows. A disabled plugin has no row, so
merely opening the Display or Rotation & Durations tab took it out of the
inputs, and the next save of that form stored the lists without it. Exclude
Clock from Vegas, disable it, change the brightness, re-enable it: Clock was
scrolling in Vegas again and had moved to the end of the rotation.

syncInputs now keeps the saved ids that have no row. In the order, each one
keeps its saved slot and the rows fill the other slots in their current
order, with rows not in the saved order last, as before. In the exclusions
they follow the unchecked rows. Only string ids are carried over, once each:
/config/main refuses a list holding anything else, which would block every
later save of the tab.

Tests: test/js/unit/test_plugin_order_list.js runs the shipped widget in a vm
with a fake DOM (draw, reorder, include/exclude, the rotation list, junk ids)
and is in run_all.js and the README. The durations DOM suite now reads only
its own rows' ids from the input, since a rig's saved order can hold others.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 20:50:05 -04:00

8.8 KiB

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_page_registry.js no The page lifecycle in js/core/registry.js (a minimal DOM shim): one init per data-page root, destroy and an aborted ctx.signal when htmx swaps it away, a vetoed swap keeps it, lazy page modules, a root removed without htmx swept on the next swap
unit/test_core_modules.js no js/core/api.js (JSON envelope, HTTP/status: error/network errors, abort passthrough, the #683 login redirect, same-server paths only) and js/core/facade.js (window.LEDMatrix, deprecated aliases)
unit/test_plugin_order_list.js no widgets/plugin-order-list.js (the Vegas and rotation order lists): a disabled plugin, which gets no row, keeps its slot in the saved order and its Vegas exclusion when the list rewrites its hidden inputs, around reordering and include/exclude; only string ids are carried over, once each
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_cache_page.js yes The Cache tab as a page module (js/pages/cache.js) on the real partial: no inline script, one request per swap and per Refresh after repeated swaps, a cancelled request draws nothing, hostile keys stay text, delete/empty/error/login states
dom/test_durations_page.js yes The Rotation tab (js/pages/durations.js) with the real plugin-order-list.js widget: one plugin-list request per swap, one move per click after repeated swaps, a swap cancels the request in flight, a late widget is waited for
dom/test_operation_history_page.js yes The Operation History tab (js/pages/operation-history.js): one request per swap and per Refresh, the plugin filter filled once, paging, filters, search, Clear, error/login states, hostile values stay text
dom/test_raw_json_page.js yes The Config Editor tab (js/pages/raw-json.js): one POST per Save after repeated swaps, Format/Validate, invalid JSON never sent, a save survives a swap, the old global entry points
dom/test_backup_restore_page.js yes The Backup & Restore tab (js/pages/backup-restore.js): one request per action after repeated swaps, the upload and restore options, reads cancelled and writes not on a swap, hostile names stay text, the old global entry points
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.