Skip to content

Research/upgrade ch5 for solid#276

Open
nbrar-crestron wants to merge 6 commits into
Crestron:masterfrom
nbrar-crestron:research/upgrade-ch5-for-solid
Open

Research/upgrade ch5 for solid#276
nbrar-crestron wants to merge 6 commits into
Crestron:masterfrom
nbrar-crestron:research/upgrade-ch5-for-solid

Conversation

@nbrar-crestron
Copy link
Copy Markdown

An AI based review.

nbrar-crestron and others added 6 commits May 20, 2026 10:43
Adds Jest 29 + ts-jest + jest-environment-jsdom as a new test runner that
lives next to the existing mocha (.spec.ts) and WCT (HTML) suites. New
behavioural / before-after tests for the SOLID upgrade go in tests/jest/.

  npm run test:jest             # one-shot
  npm run test:jest:watch       # watch mode
  npm run test:jest:coverage    # with coverage

Jest's testMatch picks up only tests/jest/**/*.test.ts and explicitly
ignores src/**/*.spec.ts, so the mocha suite is unaffected.

tsconfig.umd.json gains an exclude for tests/ — its prior **/*.ts include
was greedy enough to pull the new Jest tests into the library build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…on guard

src/ch5-list/ch5-list.ts:508 had setTimeout(..., 0.5) — almost certainly
a typo for 500. A 0.5 ms delay collapses to the next macrotask, defeating
the debounce and forcing a synchronous resizeList() on every
viewport-change event.

Adds tests/jest/perf/short-timeout.test.ts, a regression guard that walks
every src/*.ts via the TypeScript compiler API and flags any setTimeout /
setInterval call whose delay is a numeric literal in (0, 16) ms. The
threshold is one frame at 60Hz. delay === 0 is permitted as the standard
"yield to next macrotask" idiom.

Supporting helpers:
  tests/jest/_helpers/source-scanner.ts  — file walker + line/col resolver
  tests/jest/_helpers/ts-call-finder.ts  — AST-based call-site finder

The AST approach handles nested calls, multi-line arguments, and comments
correctly — a regex pass over the same code missed multi-line setTimeout
and over-matched on inner literals.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… singleton

Adds src/ch5-core/ch5-shared-mutation-observer.ts — a singleton pool that
wraps a single browser-level MutationObserver and dispatches per-target
callbacks. Modelled on the existing Ch5CoreIntersectionObserver pattern.

Refactors src/ch5-common/ch5-mutation-observer.ts to be a thin facade
that delegates to the pool. The constructor, observe(), disconnectObserver(),
isConnected field, ELEMENTS_MO_EXCEPTION static, and checkElementValidity
static are preserved exactly — so the 35+ existing call sites need no
changes.

Net effect: N components that each used to construct their own
MutationObserver now share ONE underlying observer. Proven by
tests/jest/perf/mutation-observer-facade.test.ts (50 facade instances →
1 ctor call).

Dispatch correctly handles subtree:true configurations by walking up
mutation.target's ancestor chain to find a matching registered node.

Test coverage:
  shared-mutation-observer.test.ts    7 tests — pool invariants
  mutation-observer-facade.test.ts    5 tests — back-compat + isolation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ility

Adds src/ch5-core/ch5-shared-resize-observer.ts — a singleton pool that
wraps a single browser ResizeObserver and routes entries to per-target
callbacks. Includes feature-detection so it's a no-op on platforms
without native ResizeObserver.

Refactors src/ch5-core/resize-observer.ts. The historical utility
created a new ResizeObserver per call AND returned void — every caller
leaked one observer permanently with no way to disconnect. It now:
  • delegates to Ch5SharedResizeObserver (one underlying observer)
  • returns a disposer so opt-in callers can finally clean up

The 11 existing callers that ignore the return value still work and
still benefit from pooling. Two direct consumers are migrated to use
the disposer in their removeEventListeners() path:
  src/ch5-video-switcher/ch5-video-switcher.ts
  src/ch5-signal-level-gauge/ch5-signal-level-gauge.ts

Test coverage (9 tests):
  shared-resize-observer.test.ts
    • N targets → 1 underlying ResizeObserver
    • per-target dispatch
    • disposer unregisters exactly that target
    • teardown on empty
    • graceful no-op when ResizeObserver is unavailable
    • legacy utility routes through the pool
    • legacy utility returns a working disposer
    • legacy utility still works for callers that ignore the disposer

Supporting helper:
  tests/jest/_helpers/fake-resize-observer.ts — controllable shim for JSDOM
  (JSDOM does not implement ResizeObserver natively)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…init failures

Two long-standing bugs that the test scaffolding now guards against.

1. Language-change subscription leak (src/ch5-common/ch5-common.ts)

The Ch5Common constructor subscribed to the language-change signal and
dropped the returned subscription key on the floor. The subscription
closure captured `this` (the component) AND `this.translatableObjects`,
so every component instance was pinned in memory for the lifetime of
the page — even after removal from the DOM.

Fix: track the returned key in `_languageChangeSubKey` and release it
in `unsubscribeFromSignals()` via `signal.unsubscribe(key)`. Always
released — the `_keepListeningOnSignalsAfterRemoval` flag is for
receive-state bridge subs used for re-attachment; the language sub
must not be kept alive.

Also adds `_baseClassSubscriptions: Subscription[]` as future
infrastructure for any non-bridge RxJS subscriptions a base or
subclass method might register. Drained alongside the language sub.

2. Silent catches in ColorPicker (src/ch5-color-picker/color-picker.ts)

The constructor's try/catch swallowed init failures into `joe = null`
with no log. setColor() then silently no-op'd on the null. QA and
field engineers had nothing to grep for when the picker didn't render.

Fix:
  • catches now console.warn with the picker id and the underlying error
  • renamed private `joe` → `_picker` (descriptive; nothing external
    referenced the old name)
  • added `isReady` getter so callers can check before calling
  • setColor() returns boolean (existing void-returning callers unaffected)

Test coverage:
  language-subscription.test.ts   4 tests — bridge unsubscribe, drain
                                            array, _keepListening flag,
                                            no-op when nothing tracked
  color-picker-silent-catch.test.ts 2 tests — init failure warns + isReady,
                                              setColor warns on uninit

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
eslint:
  • no-explicit-any: off → warn
  • no-console:       off → warn

Surfaces ~689 warnings across the existing source so future work can
chip away at them without blocking the build today.

package.json:
  • removes the `lint` npm script — it referenced `tslint`, but no
    tslint.json exists in the repo. eslint (script `eslint`) is the
    real linter.

MIGRATION.md:
  • developer-facing walkthrough of every change in this branch
  • how to run the new Jest harness
  • the new shared observer APIs and their migration path
  • the bridge-vs-RxJS subscription pattern in Ch5Common
  • the regression-guard tests and how to add new ones
  • what is explicitly deferred to later phases and why

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant