diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index 250cf3f7..00000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - parser: '@typescript-eslint/parser', // Specifies the ESLint parser - parserOptions: { - ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features - sourceType: 'module', // Allows for the use of imports - }, - extends: [ - 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin - 'prettier', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier - ], - rules: { - // Place to specify ESLint rules. Can be used to overwrite rules specified - // from the extended configs e.g. - // "@typescript-eslint/explicit-function-return-type": "off", - '@typescript-eslint/no-explicit-any': 'error', - 'no-multi-assign': 'error', - }, -}; diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md index d3763bba..5a6f617c 100644 --- a/INSTRUCTIONS.md +++ b/INSTRUCTIONS.md @@ -9,7 +9,7 @@ Commands: `yarn install`, `yarn run build`, `yarn run test`, `yarn run coverage` ## Code Style - Conventional Commits (used by semantic-release). -- Comments explain **why**, not what — only when intent isn't obvious from code. +- Comments explain **why**, not what -- only when intent isn't obvious from code. ## TypeScript @@ -32,7 +32,7 @@ Commands: `yarn install`, `yarn run build`, `yarn run test`, `yarn run coverage` ## Architecture - Consolidate duplicate logic when adding helpers; don't leave redundant inline expressions. -- **Naming consistency:** Renames span all layers — schema, types, CSS, localization keys, templates, docs. All schema fields must appear in the documentation table. +- **Naming consistency:** Renames span all layers -- schema, types, CSS, localization keys, templates, docs. All schema fields must appear in the documentation table. - **Separation of concerns:** `components/` renders only; logic in `components-lib/` controllers (using `ReactiveController` where needed). - **Manager pattern:** `CardController` orchestrates managers (`ConfigManager`, `HASSManager`, `ViewManager`, etc.). New cross-cutting concerns → new manager. - **Module conventions:** `types.ts` for types/schemas, `*-manager.ts` for coordinators, `*-controller.ts` for logic, `utils/` for helpers. @@ -40,7 +40,7 @@ Commands: `yarn install`, `yarn run build`, `yarn run test`, `yarn run coverage` ## Preferences - Think through UX states and edge cases **before** writing code. -- No re-exports or pass-through files — import from source directly. +- No re-exports or pass-through files -- import from source directly. - Derive TypeScript types from Zod schemas (`z.infer`); no parallel interfaces. - User-facing schemas: only user-configurable fields. Internal fields go in a derived interface (`interface InternalFoo extends Foo { ... }`). - Shared schemas go in `config/schema/common/` from the start. diff --git a/docs/configuration/profiles.md b/docs/configuration/profiles.md index c98eecc4..50080f92 100644 --- a/docs/configuration/profiles.md +++ b/docs/configuration/profiles.md @@ -39,7 +39,7 @@ Turns the card into a phone-style ringer that answers a [two-way audio call](../usage/2-way-audio.md) when somebody presses a doorbell. Intended for a wall-mounted tablet sitting on a dashboard with a doorbell camera. -Minimal configuration — just point it at a doorbell-capable camera: +Minimal configuration -- just point it at a doorbell-capable camera: ```yaml type: custom:advanced-camera-card diff --git a/docs/configuration/status-bar.md b/docs/configuration/status-bar.md index e3af3452..ab8b0f35 100644 --- a/docs/configuration/status-bar.md +++ b/docs/configuration/status-bar.md @@ -29,14 +29,14 @@ status_bar: ### Available Items -| Button name | Description | -| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `engine` | The icon of the camera engine for the relevant camera. | -| `issues` | Indicator icons that appear while any card issue is active — e.g. configuration error, configuration upgrade available, Home Assistant connection lost, camera initialization failed, legacy `frigate-hass-card` resource detected, media (live/recorded/image) not loading, media query failed, or view cannot be resolved. See the warning below about disabling this item. | -| `resolution` | The detected media resolution (if any). | -| `severity` | The media severity indicator (if any) for review severity (e.g. Frigate alerts/detections). | -| `technology` | The detected media technology (if any). | -| `title` | The media title. | +| Button name | Description | +| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `engine` | The icon of the camera engine for the relevant camera. | +| `issues` | Indicator icons that appear while any card issue is active -- e.g. configuration error, configuration upgrade available, Home Assistant connection lost, camera initialization failed, legacy `frigate-hass-card` resource detected, media (live/recorded/image) not loading, media query failed, or view cannot be resolved. See the warning below about disabling this item. | +| `resolution` | The detected media resolution (if any). | +| `severity` | The media severity indicator (if any) for review severity (e.g. Frigate alerts/detections). | +| `technology` | The detected media technology (if any). | +| `title` | The media title. | ### Options for each item diff --git a/docs/examples.md b/docs/examples.md index 0491f1ae..81629bbc 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -91,7 +91,7 @@ automations: ### Fullscreen with display mode and substream switching This example adds a custom menu button that switches to single display mode, -activates the HD substream, and enters card fullscreen — all in a single tap. +activates the HD substream, and enters card fullscreen -- all in a single tap. An automation restores the grid layout and substream when fullscreen is exited. This is useful in [grid](configuration/live.md) layouts where the card's standard fullscreen button would show all cameras rather than a single HD @@ -376,9 +376,9 @@ elements: The [`doorbell` profile](configuration/profiles.md?id=doorbell) turns a dashboard into a phone-like ringer when somebody presses the doorbell, by setting [`view.triggers.actions.trigger: call`](configuration/view.md?id=triggers) and auto-discovering [HA `event.*` entities](https://www.home-assistant.io/integrations/event/#device-class) with `device_class: doorbell` on the camera's device (Ring, UniFi Protect, Nest, DoorBird, Reolink, etc.). The intended deployment is a wall-mounted tablet sitting on the dashboard. -A doorbell press is instantaneous, so the card synthesises a ring window from [`view.triggers.event_hold_seconds`](configuration/view.md?id=triggers) (default `30`s) — long enough for a typical phone-style answer window. `untrigger_delay_seconds` then lingers past that, same as for any stateful trigger. +A doorbell press is instantaneous, so the card synthesises a ring window from [`view.triggers.event_hold_seconds`](configuration/view.md?id=triggers) (default `30`s) -- long enough for a typical phone-style answer window. `untrigger_delay_seconds` then lingers past that, same as for any stateful trigger. -`triggers.motion`, `triggers.occupancy`, and `triggers.media_events` are off by default — only the explicit doorbell press triggers the call, so casual motion won't make the card ring. +`triggers.motion`, `triggers.occupancy`, and `triggers.media_events` are off by default -- only the explicit doorbell press triggers the call, so casual motion won't make the card ring. ```yaml type: custom:advanced-camera-card @@ -398,9 +398,9 @@ profiles: #### With a Zigbee (ZHA / deCONZ) doorbell button -Zigbee buttons connected via ZHA or deCONZ typically don't expose a per-device entity — they fire raw HA bus events (`zha_event`, `deconz_event`) shared across every Zigbee device on the integration. The `doorbell` profile's auto-discovery doesn't cover this case; reuse the profile (it still wires up `trigger: call` / `untrigger: call` and the ring window) but opt out of auto-discovery per camera and add [`triggers.events`](configuration/cameras/README.md?id=events) with an `event_data` filter to pick out the right device. +Zigbee buttons connected via ZHA or deCONZ typically don't expose a per-device entity -- they fire raw HA bus events (`zha_event`, `deconz_event`) shared across every Zigbee device on the integration. The `doorbell` profile's auto-discovery doesn't cover this case; reuse the profile (it still wires up `trigger: call` / `untrigger: call` and the ring window) but opt out of auto-discovery per camera and add [`triggers.events`](configuration/cameras/README.md?id=events) with an `event_data` filter to pick out the right device. -You can copy the exact `device_ieee` (ZHA) or `id` (deCONZ) and command values straight out of **Developer tools → Events** in Home Assistant — listen to the event type, press the doorbell, and use whatever appears in the `data` payload. +You can copy the exact `device_ieee` (ZHA) or `id` (deCONZ) and command values straight out of **Developer tools → Events** in Home Assistant -- listen to the event type, press the doorbell, and use whatever appears in the `data` payload. ```yaml type: custom:advanced-camera-card diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index a8f60578..34156328 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -233,7 +233,7 @@ between clicking the download button and the download starting. - If you are using a custom `client_id` setting in your `frigate.yml` file (the configuration file for the Frigate backend itself), the card will auto-detect it from the camera entity. If auto-detection fails (e.g. no `camera_entity` - is configured), set it manually — see [Frigate engine + is configured), set it manually -- see [Frigate engine configuration](configuration/cameras/engine.md?id=frigate). - You must have the `Enable the media browser` option enabled for the Frigate integration, in order for media fetches to work for the card. Media fetches @@ -305,7 +305,7 @@ Picture-in-Picture (PIP) uses the browser's [native video PIP API](https://caniuse.com/picture-in-picture) which floats the raw video element into a small window. This means: -- **No card UI in the PIP window.** Only the video itself is shown — no menu, +- **No card UI in the PIP window.** Only the video itself is shown -- no menu, status bar, timeline, or other card elements. The card remains fully functional on the dashboard behind it. - **Limited browser support.** Not all browsers support the PIP API. See @@ -320,7 +320,7 @@ was explored using the experimental [Document Picture-in-Picture API](https://caniuse.com/mdn-api_documentpictureinpicture), however it proved unworkable: Home Assistant state updates cannot reach a card in a separate document, and browser-managed styles (`adoptedStyleSheets`) are cleared when -elements move between documents — resulting in an unstyled, non-updating card. +elements move between documents -- resulting in an unstyled, non-updating card. ### New version not working in Chrome diff --git a/docs/uml/call-sequence.puml b/docs/uml/call-sequence.puml index 8dffe2f0..804cf9d7 100644 --- a/docs/uml/call-sequence.puml +++ b/docs/uml/call-sequence.puml @@ -25,7 +25,7 @@ opt **""live.microphone.always_connected""** not enabled end note over User, Mic - The microphone must connect for the call to proceed — + The microphone must connect for the call to proceed -- if access is denied or unsupported, the call does not start end note @@ -46,8 +46,8 @@ end User -> Card : End the call note over User, Card - Via Hang-up button, **""call_end""** action, or — when - **""live.controls.call.lock""** is disabled — navigating away + Via Hang-up button, **""call_end""** action, or -- when + **""live.controls.call.lock""** is disabled -- navigating away end note Card --> User : Inbound audio mutes (**""live.auto_mute""**) diff --git a/eslint.config.mjs b/eslint.config.mjs index a0b1811c..b5bca790 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -13,6 +13,39 @@ const compat = new FlatCompat({ allConfig: js.configs.all, }); +// Local rule: ban the em-dash character (U+2014) anywhere, including comments and +// strings. The codebase uses a colon or a double hyphen `--` instead. The regex uses +// the unicode escape so the character never appears literally in this file. +const noEmDash = { + meta: { + type: 'problem', + docs: { description: 'Disallow the em-dash character; use ":" or "--" instead.' }, + messages: { + emDash: 'Em-dash character is not allowed; use ":" or "--" instead.', + }, + }, + create(context) { + const sourceCode = context.sourceCode ?? context.getSourceCode(); + return { + Program(node) { + const text = sourceCode.getText(); + const re = /\u2014/g; + let match; + while ((match = re.exec(text)) !== null) { + context.report({ + node, + loc: { + start: sourceCode.getLocFromIndex(match.index), + end: sourceCode.getLocFromIndex(match.index + 1), + }, + messageId: 'emDash', + }); + } + }, + }; + }, +}; + export default defineConfig([ { extends: compat.extends('plugin:@typescript-eslint/recommended', 'prettier'), @@ -23,8 +56,56 @@ export default defineConfig([ sourceType: 'module', }, + plugins: { + local: { rules: { 'no-em-dash': noEmDash } }, + }, + rules: { curly: 'error', + 'local/no-em-dash': 'error', + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-non-null-assertion': 'error', + '@typescript-eslint/parameter-properties': 'error', + }, + }, + + // Timers must go through the Timer class (src/utils/timer.ts). + { + files: ['src/**/*.ts'], + rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: + 'CallExpression > Identifier.callee[name=/^(setTimeout|setInterval)$/]', + message: + 'Use the Timer class (src/utils/timer.ts) instead of setTimeout/setInterval.', + }, + { + selector: 'MemberExpression[property.name=/^(setTimeout|setInterval)$/]', + message: + 'Use the Timer class (src/utils/timer.ts) instead of window.setTimeout/setInterval.', + }, + ], + }, + }, + + // A fire-and-forget promise must be marked with `void` to show it is deliberate. + // To know which calls return a promise, this rule needs type information, so the + // `project` option below runs the TypeScript type-checker over source while linting. + // That makes linting source slower. Tests are excluded: they leave promises unawaited + // freely. + { + files: ['src/**/*.ts'], + languageOptions: { + parser: tsParser, + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + }, + }, + rules: { + '@typescript-eslint/no-floating-promises': 'error', }, }, ]); diff --git a/scripts/deployment-update-symlink.sh b/scripts/deployment-update-symlink.sh index 76953636..b20cc68d 100755 --- a/scripts/deployment-update-symlink.sh +++ b/scripts/deployment-update-symlink.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -# `yarn deployment-update` — point the manual-test Home Assistant instance at +# `yarn deployment-update` -- point the manual-test Home Assistant instance at # this worktree. # # Repoints the `$ACC_LIVE_LINK` symlink (the dev static server's document root) @@ -9,7 +9,7 @@ set -euo pipefail if [[ -z "${ACC_LIVE_LINK:-}" ]]; then - echo "ACC_LIVE_LINK is not set — point it at the symlink to update." >&2 + echo "ACC_LIVE_LINK is not set -- point it at the symlink to update." >&2 exit 1 fi @@ -18,4 +18,4 @@ mkdir -p "$(dirname "$ACC_LIVE_LINK")" ln -sfn "$TARGET" "$ACC_LIVE_LINK" echo "Advanced Camera Card → $TARGET" -[[ -d "$TARGET" ]] || echo "(no dist/ here yet — run \`yarn start\` to build it)" +[[ -d "$TARGET" ]] || echo "(no dist/ here yet -- run \`yarn start\` to build it)" diff --git a/src/card-controller/actions/actions/effect.ts b/src/card-controller/actions/actions/effect.ts index f6019f32..b5e36209 100644 --- a/src/card-controller/actions/actions/effect.ts +++ b/src/card-controller/actions/actions/effect.ts @@ -9,13 +9,13 @@ export class EffectAction extends AdvancedCameraCardAction { const action = this._getAction(); switch (action.effect_action) { case 'start': - api.getEffectsManager().startEffect(action.effect); + void api.getEffectsManager().startEffect(action.effect); break; case 'stop': api.getEffectsManager().stopEffect(action.effect); break; case 'toggle': - api.getEffectsManager().toggleEffect(action.effect); + void api.getEffectsManager().toggleEffect(action.effect); break; } } diff --git a/src/card-controller/actions/actions/ptz-controls.ts b/src/card-controller/actions/actions/ptz-controls.ts index 08d7a19b..eb86054e 100644 --- a/src/card-controller/actions/actions/ptz-controls.ts +++ b/src/card-controller/actions/actions/ptz-controls.ts @@ -11,7 +11,7 @@ export class PTZControlsAction extends AdvancedCameraCardAction( @@ -189,7 +189,7 @@ export class ConfigManager { (config) => [config.cameras, config.cameras_global], () => { this._api.getInitializationManager().uninitialize(InitializationAspect.CAMERAS); - this._api.getCameraManager().destroy(); + void this._api.getCameraManager().destroy(); }, true, ); @@ -203,7 +203,7 @@ export class ConfigManager { true, ); - /* async */ this._initializeBackgroundAndUpdate(previousConfig); + void this._initializeBackgroundAndUpdate(previousConfig); } /** diff --git a/src/card-controller/config/overrides-manager.ts b/src/card-controller/config/overrides-manager.ts index 03cefd89..5b5eb4bd 100644 --- a/src/card-controller/config/overrides-manager.ts +++ b/src/card-controller/config/overrides-manager.ts @@ -92,7 +92,7 @@ export class OverridesManager { const parseResult = advancedCameraCardConfigSchema.safeParse(output); if (!parseResult.success) { - // Surface one co-located failure object per Zod issue — path, the value + // Surface one co-located failure object per Zod issue -- path, the value // the user actually wrote, and the most informative "expected" field for // this issue code. Avoids dumping the full merged config (which is // mostly schema defaults the user never wrote) and keeps the reader from diff --git a/src/card-controller/effects/effects-manager.ts b/src/card-controller/effects/effects-manager.ts index 604fcb05..86800a9b 100644 --- a/src/card-controller/effects/effects-manager.ts +++ b/src/card-controller/effects/effects-manager.ts @@ -166,7 +166,7 @@ export class EffectsManager implements EffectsManagerInterface { private _startPendingEffects(): void { for (const [name, options] of this._pendingEffects.entries()) { this._pendingEffects.delete(name); - this._startEffect(name, options); + void this._startEffect(name, options); } } } diff --git a/src/card-controller/fullscreen/screenfull/index.ts b/src/card-controller/fullscreen/screenfull/index.ts index 03a02f16..46d79c7f 100644 --- a/src/card-controller/fullscreen/screenfull/index.ts +++ b/src/card-controller/fullscreen/screenfull/index.ts @@ -32,9 +32,12 @@ export class ScreenfullFullScreenProvider } if (fullscreen) { - screenfull.request(this._api.getCardElementManager().getElement()); + // A denied request (or an exit when not in fullscreen) leaves the UI + // consistent: the 'change' handler only fires on a real transition. + // Nothing to act on. + screenfull.request(this._api.getCardElementManager().getElement()).catch(() => {}); } else { - screenfull.exit(); + screenfull.exit().catch(() => {}); } } } diff --git a/src/card-controller/fullscreen/webkit/index.ts b/src/card-controller/fullscreen/webkit/index.ts index 112d249a..a52a1a06 100644 --- a/src/card-controller/fullscreen/webkit/index.ts +++ b/src/card-controller/fullscreen/webkit/index.ts @@ -85,7 +85,10 @@ export class WebkitFullScreenProvider // clicking the 'X' which then fires this event). That's probably the rare // case though. this._playTimer.start(WEBKIT_PLAY_SECONDS, () => { - this._getVideoElement()?.play(); + // Best-effort resume after a fullscreen exit. + this._getVideoElement() + ?.play() + .catch(() => {}); }); }; } diff --git a/src/card-controller/hass/hass-manager.ts b/src/card-controller/hass/hass-manager.ts index 8edd361d..ced3a719 100644 --- a/src/card-controller/hass/hass-manager.ts +++ b/src/card-controller/hass/hass-manager.ts @@ -61,7 +61,7 @@ export class HASSManager implements HASSManagerReadonlyInterface { ); this._api.getInitializationManager().uninitialize(InitializationAspect.CAMERAS); - this._api.getCameraManager().destroy(); + void this._api.getCameraManager().destroy(); this._api.getInitializationManager().uninitialize(InitializationAspect.VIEW); this._api .getInitializationManager() diff --git a/src/card-controller/initialization-manager.ts b/src/card-controller/initialization-manager.ts index 266c915a..21743dbc 100644 --- a/src/card-controller/initialization-manager.ts +++ b/src/card-controller/initialization-manager.ts @@ -80,7 +80,7 @@ export class InitializationManager { if (!this._shouldInitializeMandatory()) { return; } - /* async */ this.initializeMandatory(); + void this.initializeMandatory(); } private _shouldInitializeMandatory(): boolean { diff --git a/src/card-controller/issues/issue-manager.ts b/src/card-controller/issues/issue-manager.ts index 7e3c57bf..bdca7be6 100644 --- a/src/card-controller/issues/issue-manager.ts +++ b/src/card-controller/issues/issue-manager.ts @@ -29,7 +29,7 @@ export class IssueManager { private _suspended = false; // Reentrancy guard: evaluate() calls setState() on the condition state - // manager, which fires listeners synchronously — including the one + // manager, which fires listeners synchronously -- including the one // registered in this constructor. Without this guard, detectDynamic() // and presence computation would run twice per evaluation. private _evaluating = false; @@ -165,16 +165,14 @@ export class IssueManager { // normal re-evaluation (on any condition-state change). // // `initialized: true` in the change payload means mandatory initialization - // just finished — see InitializationManager._initializeMandatory. That's + // just finished -- see InitializationManager._initializeMandatory. That's // also the earliest point at which the full HASS object is guaranteed // ready for websocket calls (e.g. LegacyResourceIssue's lovelace/resources // fetch). Because `initialized` is latched (its comment notes it never // changes again), this block fires exactly once per IssueManager life. private _onStateChange(change: ConditionStateChange): void { if (change.change.initialized === true && change.new.hass) { - /* async */ this._stateManager - .detectStatic(change.new.hass) - .then(() => this.evaluate()); + void this._stateManager.detectStatic(change.new.hass).then(() => this.evaluate()); } this.evaluate(); } diff --git a/src/card-controller/issues/issues/connection.ts b/src/card-controller/issues/issues/connection.ts index ae34e2db..f012e703 100644 --- a/src/card-controller/issues/issues/connection.ts +++ b/src/card-controller/issues/issues/connection.ts @@ -19,7 +19,7 @@ export class ConnectionIssue implements Issue { private _state: ConnectionState = 'ready'; public detectDynamic(state: ConditionState): void { - // Before HASS is ever provided, leave state untouched — undefined hass is + // Before HASS is ever provided, leave state untouched -- undefined hass is // not a disconnection, just "not yet initialized". if (state.hass === undefined) { return; diff --git a/src/card-controller/issues/issues/initialization.ts b/src/card-controller/issues/issues/initialization.ts index e7b9e9d6..0ed4ff8f 100644 --- a/src/card-controller/issues/issues/initialization.ts +++ b/src/card-controller/issues/issues/initialization.ts @@ -44,7 +44,7 @@ export class InitializationIssue extends AbstractErrorIssue { // resources (WebSocket subscriptions, listeners) before the CAMERAS // init aspect replaces the instance via createCameraManager(). this._api.getInitializationManager().uninitializeMandatory(); - this._api.getCameraManager().destroy(); + void this._api.getCameraManager().destroy(); return false; } diff --git a/src/card-controller/issues/issues/media-load.ts b/src/card-controller/issues/issues/media-load.ts index d11d627b..a81893bc 100644 --- a/src/card-controller/issues/issues/media-load.ts +++ b/src/card-controller/issues/issues/media-load.ts @@ -37,7 +37,7 @@ export class MediaLoadIssue implements Issue { } // ========================================================================= - // Explicit trigger — called when a component fires an issue:trigger event. + // Explicit trigger -- called when a component fires an issue:trigger event. // ========================================================================= public trigger(context: IssueTriggerContext['media_load']): void { @@ -45,7 +45,7 @@ export class MediaLoadIssue implements Issue { } // ========================================================================= - // Detection — called by the manager on every state change. + // Detection -- called by the manager on every state change. // ========================================================================= public detectDynamic(state: ConditionState): void { @@ -62,7 +62,7 @@ export class MediaLoadIssue implements Issue { } // ========================================================================= - // State queries — called by the manager to read current state. + // State queries -- called by the manager to read current state. // ========================================================================= public hasIssue(): boolean { @@ -113,7 +113,7 @@ export class MediaLoadIssue implements Issue { } // ========================================================================= - // Retry — called by the manager to schedule a media reload. + // Retry -- called by the manager to schedule a media reload. // ========================================================================= public needsRetry(): boolean { @@ -144,7 +144,7 @@ export class MediaLoadIssue implements Issue { // re-attempts loading underneath. If the retry succeeds, // _handleMediaLoaded will clear everything when media:loaded fires. If // it fails silently (e.g. bogus stream name), the error stays visible - // immediately — no new 10s grace period. + // immediately -- no new 10s grace period. this._api.getViewManager().setViewWithMergedContext({ mediaEpoch }); return false; } @@ -212,7 +212,7 @@ export class MediaLoadIssue implements Issue { this._timerTargetID = targetID; this._timer.start(MEDIA_LOADING_TIMEOUT_SECONDS, () => { // Record the error on timeout so retry() knows which epoch to bump. - // targetID is guaranteed non-null here — the null case bails at the + // targetID is guaranteed non-null here -- the null case bails at the // top of _handleMediaNotLoaded. this._erroredTargetIDs.add(targetID); this._activate(); diff --git a/src/card-controller/issues/issues/media-query.ts b/src/card-controller/issues/issues/media-query.ts index a7a30228..82cab767 100644 --- a/src/card-controller/issues/issues/media-query.ts +++ b/src/card-controller/issues/issues/media-query.ts @@ -31,7 +31,7 @@ export class MediaQueryIssue extends AbstractErrorIssue { return false; } this._error = null; - this._api.getViewManager().setViewByParametersWithNewQuery(); + void this._api.getViewManager().setViewByParametersWithNewQuery(); // Exclusive retry. No other issue should attempt to retry until the next // evaluation cycle, when we'll know if this was successful. diff --git a/src/card-controller/issues/state-manager.ts b/src/card-controller/issues/state-manager.ts index a4a8005c..37a299b9 100644 --- a/src/card-controller/issues/state-manager.ts +++ b/src/card-controller/issues/state-manager.ts @@ -3,7 +3,7 @@ import { summarizeNotification } from '../../components-lib/notification/summari import { ConditionState } from '../../condition-trigger/conditions/types'; import { Notification } from '../../config/schema/actions/types'; import { HomeAssistant } from '../../ha/types'; -import { isTruthy } from '../../utils/basic'; +import { errorToConsole, isTruthy } from '../../utils/basic'; import { Issue, IssueDescription, @@ -27,12 +27,18 @@ export class IssueStateManager implements IssueReadOnlyState { } // ========================================================================= - // Detection — static (one-shot on init) and dynamic (on every state change). + // Detection -- static (one-shot on init) and dynamic (on every state change). // ========================================================================= public async detectStatic(hass: HomeAssistant): Promise { for (const issue of this._issues.values()) { - await issue.detectStatic?.(hass); + try { + await issue.detectStatic?.(hass); + } catch (e) { + // Isolate one issue's detection failure so it cannot abort detection + // for the rest; log so the cause is visible. + errorToConsole(e as Error); + } this._logIfNew(issue); } } @@ -57,7 +63,7 @@ export class IssueStateManager implements IssueReadOnlyState { } // ========================================================================= - // Queries — read active issue state. + // Queries -- read active issue state. // ========================================================================= public getFullCardIssue(): IssueDescription | null { diff --git a/src/card-controller/issues/types.ts b/src/card-controller/issues/types.ts index c1d4adc5..b013cf75 100644 --- a/src/card-controller/issues/types.ts +++ b/src/card-controller/issues/types.ts @@ -28,11 +28,11 @@ export interface KeyedIssueDescription { } // Map of currently active issues keyed by IssueKey, with each entry's value -// being the issue's current rendered description. Stored as a Map (not just -// a Set of keys) so that sub-state changes within an issue — e.g. -// ConnectionIssue swapping between 'lost' and 'starting' — are reflected as -// real value-level diffs to the condition state, triggering re-renders and -// any user-defined conditions that depend on issue state. +// being the issue's current rendered description. Stored as a Map (not just a +// Set of keys) so that sub-state changes within an issue -- e.g. +// ConnectionIssue swapping between 'lost' and 'starting' -- are reflected as +// real value-level diffs to the condition state, triggering re-renders and any +// user-defined conditions that depend on issue state. export type IssuePresence = Map; export interface IssueReadOnlyState { hasFullCardIssue(): boolean; @@ -80,7 +80,7 @@ export interface Issue { // loop (exclusive), false to allow subsequent issues to also retry. retry?(): boolean; - // Optional user-initiated fix. Not called by the issue infrastructure — + // Optional user-initiated fix. Not called by the issue infrastructure -- // callers (e.g. notification control actions) invoke this directly. fix?(hass: HomeAssistant): Promise; @@ -92,11 +92,11 @@ export interface Issue { // Called when the card is detached. Issues with age-based timers (e.g. // loading-timeout timers) must stop them here so that time spent offscreen - // doesn't count against the user. Must preserve already-active issue state - // — a full-card issue visible at detach should still be visible on - // reattach. No `resume` hook: IssueManager.resume() triggers a normal - // evaluate(), so any timer that should restart is re-armed via - // detectDynamic against the current condition state. + // doesn't count against the user. Must preserve already-active issue state -- + // a full-card issue visible at detach should still be visible on reattach. No + // `resume` hook: IssueManager.resume() triggers a normal evaluate(), so any + // timer that should restart is re-armed via detectDynamic against the current + // condition state. suspend?(): void; // Release external resources (e.g. a listener registered on another manager) diff --git a/src/card-controller/lock/call-policy.ts b/src/card-controller/lock/call-policy.ts index dae8d1dd..a8d179aa 100644 --- a/src/card-controller/lock/call-policy.ts +++ b/src/card-controller/lock/call-policy.ts @@ -10,8 +10,8 @@ import type { LockPolicy } from './types'; // - Stream-stopping / re-init actions: pause, reload, and casting (which // rehosts the stream to a media player). // -// `call_start` is intentionally absent — it's the entry into the lock. -// `call_end` is also absent — it dispatches via `setViewByParameters({ force: +// `call_start` is intentionally absent -- it's the entry into the lock. +// `call_end` is also absent -- it dispatches via `setViewByParameters({ force: // true })` to bypass the lock, so listing it here would be redundant. const CALL_DISRUPTIVE_ACTIONS: ReadonlySet = new Set([ // View / camera / substream changes. diff --git a/src/card-controller/media-info-manager.ts b/src/card-controller/media-info-manager.ts index 77478c16..e62db082 100644 --- a/src/card-controller/media-info-manager.ts +++ b/src/card-controller/media-info-manager.ts @@ -27,7 +27,7 @@ export class MediaLoadedInfoManager { // never cleared by `clear` / `_clearTarget`, only by `initialize`. private _lastKnown: Map = new Map(); - // The currently "active" target — the one whose info drives condition state + // The currently "active" target -- the one whose info drives condition state // and card-level side effects. Driven by ViewManager on every view change. private _selected: string | null = null; diff --git a/src/card-controller/query-string-manager.ts b/src/card-controller/query-string-manager.ts index 55c29792..1ab069a5 100644 --- a/src/card-controller/query-string-manager.ts +++ b/src/card-controller/query-string-manager.ts @@ -17,8 +17,8 @@ interface QueryStringViewIntent { // The substream change to apply alongside the view. Tri-state: // - `undefined`: no substream URL action present, no modifier issued. - // - `string`: `substream_on=X` — engage stream X. - // - `null`: `substream_off` — explicitly clear the override. + // - `string`: `substream_on=X` -- engage stream X. + // - `null`: `substream_off` -- explicitly clear the override. stream?: string | null; }; other?: AdvancedCameraCardCustomActionConfig[]; diff --git a/src/card-controller/view/view-manager.ts b/src/card-controller/view/view-manager.ts index 394df02f..5b1f6dc1 100644 --- a/src/card-controller/view/view-manager.ts +++ b/src/card-controller/view/view-manager.ts @@ -126,7 +126,7 @@ export class ViewManager implements ViewManagerInterface { ...options, }); // A non-throwing factory call clears any prior view_incompatible / - // media_query state — ensures a previously-dismissed mid-session popup + // media_query state -- ensures a previously-dismissed mid-session popup // does not linger invisibly and re-pop on the next evaluation cycle, // and that a stale media_query failure from an abandoned gallery / // viewer doesn't follow the user into an unrelated view. @@ -361,7 +361,7 @@ export class ViewManager implements ViewManagerInterface { if (!this._api.getQueryStringManager().hasViewRelatedActionsToRun()) { // This is not awaited to allow the initialization to complete before the // query is answered. - this.setViewDefaultWithNewQuery({ failSafe: true }); + void this.setViewDefaultWithNewQuery({ failSafe: true }); } }; diff --git a/src/card.ts b/src/card.ts index 7031a0a4..3b81cad9 100644 --- a/src/card.ts +++ b/src/card.ts @@ -329,7 +329,7 @@ class AdvancedCameraCard extends LitElement { protected updated(): void { if (this._controller.getInitializationManager().isInitializedMandatory()) { - this._controller.getQueryStringManager().executeIfNecessary(); + void this._controller.getQueryStringManager().executeIfNecessary(); } } diff --git a/src/components-lib/live/microphone-actions-controller.ts b/src/components-lib/live/microphone-actions-controller.ts index fe43563d..6c6f9e7f 100644 --- a/src/components-lib/live/microphone-actions-controller.ts +++ b/src/components-lib/live/microphone-actions-controller.ts @@ -56,7 +56,7 @@ export class MicrophoneActionsController { } this._callAnswered = answered; if (answered) { - this._unmuteIfConfigured('call'); + void this._unmuteIfConfigured('call'); } else { this._muteIfConfigured('call'); } @@ -106,7 +106,10 @@ export class MicrophoneActionsController { this._options?.microphoneManager && this._options.autoUnmuteConditions?.includes(condition) ) { - await this._options.microphoneManager.unmute(); + // A denied or missing microphone already shows in the UI: the menu + // microphone button switches to its forbidden icon. A failed auto-unmute + // has nothing more to act on. + await this._options.microphoneManager.unmute().catch(() => {}); } } diff --git a/src/components-lib/media-actions-controller.ts b/src/components-lib/media-actions-controller.ts index 0fe5d453..b44e6768 100644 --- a/src/components-lib/media-actions-controller.ts +++ b/src/components-lib/media-actions-controller.ts @@ -68,7 +68,7 @@ export class MediaActionsController { public setMicrophoneState(state: MicrophoneState): void { const previous = this._microphoneState; this._microphoneState = state; - this._microphoneStateChangeHandler(previous, state); + void this._microphoneStateChangeHandler(previous, state); } // Audio-out auto-mute/unmute driven by call answer: unmute when the call @@ -85,10 +85,10 @@ export class MediaActionsController { this._callAnswered = answered; if (answered) { this._pendingCallStartAction = true; - this._applyPendingCallStartAction(); + void this._applyPendingCallStartAction(); } else { this._pendingCallStartAction = false; - this._muteTargetIfConfigured('call'); + void this._muteTargetIfConfigured('call'); } } diff --git a/src/components-lib/media-filter-controller.ts b/src/components-lib/media-filter-controller.ts index e89506c8..12c06b89 100644 --- a/src/components-lib/media-filter-controller.ts +++ b/src/components-lib/media-filter-controller.ts @@ -260,7 +260,7 @@ export class MediaFilterController { const queryCameraIDs = query.getAllCameraIDs(); const cameraID = queryCameraIDs.size === 1 ? [...queryCameraIDs][0] : undefined; - this._viewManager?.setViewByParametersWithExistingQuery({ + void this._viewManager?.setViewByParametersWithExistingQuery({ params: { query, // If single camera, set it as the active camera for menu navigation diff --git a/src/components-lib/media-loaded-info-sink-controller.ts b/src/components-lib/media-loaded-info-sink-controller.ts index 22c411ec..52c302e3 100644 --- a/src/components-lib/media-loaded-info-sink-controller.ts +++ b/src/components-lib/media-loaded-info-sink-controller.ts @@ -9,7 +9,7 @@ interface MediaLoadedInfoSinkConfig { // notification. getTargetID: () => string | null; - // Fires when the active info changes — i.e. when the active target's entry + // Fires when the active info changes -- i.e. when the active target's entry // transitions (load arrives, abort, or selection changes the active entry). // Loads for other targets are cached but do not fire the callback. callback?: (info: MediaLoadedInfo | null) => void; @@ -28,7 +28,7 @@ interface MediaLoadedInfoSinkConfig { * user selects a slide whose media has already loaded, the sink immediately * exposes the cached entry. * - * Lifecycle asymmetry — `callback` fires on: + * Lifecycle asymmetry -- `callback` fires on: * - the active target's load arrival, * - the active target's source aborting (with `null`), and * - selection changing to / from a target whose active info differs. @@ -46,7 +46,7 @@ export class MediaLoadedInfoSinkController implements ReactiveController { // stale abort can't blow away an entry that's since been overwritten. private _byTarget = new Map(); - // The targetID whose info we last surfaced — drives `hostUpdated` change + // The targetID whose info we last surfaced -- drives `hostUpdated` change // detection. `_lastActiveInfo` records what the callback last saw, so we // don't fire it for no-op selection changes (e.g. selection changes but // both old and new are null/loaded with the same info reference). @@ -67,7 +67,7 @@ export class MediaLoadedInfoSinkController implements ReactiveController { } public hostUpdated(): void { - // Detect selection changes — `getTargetID` is owned by the host and may + // Detect selection changes -- `getTargetID` is owned by the host and may // flip when its props change (carousel slide change, view change, etc.). const newID = this._config.getTargetID(); if (newID !== this._lastActiveTargetID) { diff --git a/src/components-lib/media-loaded-info-source-controller.ts b/src/components-lib/media-loaded-info-source-controller.ts index cf934a6b..91e4600c 100644 --- a/src/components-lib/media-loaded-info-source-controller.ts +++ b/src/components-lib/media-loaded-info-source-controller.ts @@ -53,7 +53,7 @@ export class MediaLoadedInfoSourceController implements ReactiveController { private _abort: AbortController | null = null; // Survives disconnect so we can re-dispatch on reconnect. Only ever holds - // info validated by `set` — i.e., always has a targetID. + // info validated by `set` -- i.e., always has a targetID. private _lastSet: TargetedMediaLoadedInfo | null = null; constructor( @@ -67,7 +67,7 @@ export class MediaLoadedInfoSourceController implements ReactiveController { public hostConnected(): void { // Two early-returns: - // - `!_lastSet`: nothing to replay — either the host has never seen a + // - `!_lastSet`: nothing to replay -- either the host has never seen a // media load or the cache was discarded as stale on a prior reconnect // (see below). // - `_abort` non-null: a dispatch is already live, meaning we're already @@ -79,7 +79,7 @@ export class MediaLoadedInfoSourceController implements ReactiveController { return; } - // Revalidate against the current targetID — the host's property may have + // Revalidate against the current targetID -- the host's property may have // flipped while we were disconnected. Replaying the cached info under a // stale targetID would misregister with the manager. if (this._lastSet.targetID === this._config.getTargetID()) { diff --git a/src/components-lib/navigation.ts b/src/components-lib/navigation.ts index d1a48f35..daa0a4fd 100644 --- a/src/components-lib/navigation.ts +++ b/src/components-lib/navigation.ts @@ -55,7 +55,7 @@ export const navigateUp = (options?: FolderNavigationParamaters | null): void => }, ); - options?.viewManagerEpoch.manager.setViewByParametersWithExistingQuery({ + void options?.viewManagerEpoch.manager.setViewByParametersWithExistingQuery({ params: { query }, }); }; @@ -77,7 +77,7 @@ export const navigateToFolder = ( }, ); - options?.viewManagerEpoch?.manager.setViewByParametersWithExistingQuery({ + void options?.viewManagerEpoch?.manager.setViewByParametersWithExistingQuery({ params: { query }, }); }; diff --git a/src/components-lib/ptz/drag-controller.ts b/src/components-lib/ptz/drag-controller.ts index 41fdb55e..85db360c 100644 --- a/src/components-lib/ptz/drag-controller.ts +++ b/src/components-lib/ptz/drag-controller.ts @@ -48,7 +48,7 @@ export class PTZDragController implements ReactiveController { // continuous mode. Below that threshold, drag-end dispatches relative. private _continuous = false; - // When a pinch occurs mid-drag, the drag is "poisoned" — all remaining + // When a pinch occurs mid-drag, the drag is "poisoned" -- all remaining // drag events for that gesture are ignored to prevent stray pan/tilt. private _dragCancelledByPinch = false; diff --git a/src/components-lib/signed-url-controller.ts b/src/components-lib/signed-url-controller.ts index 19c2fbc2..9c9558fc 100644 --- a/src/components-lib/signed-url-controller.ts +++ b/src/components-lib/signed-url-controller.ts @@ -64,7 +64,7 @@ export class SignedURLController implements ReactiveController { // When the endpoint requires signing or proxying, the URL must go through // the async resolution path. For proxied URLs, under no circumstances - // should we fall back to returning the unproxied URL — doing so risks + // should we fall back to returning the unproxied URL -- doing so risks // leaking traffic or causing mixed-content errors. if (options.proxyConfig?.enabled || options.endpoint?.sign) { return this._value; diff --git a/src/components-lib/timeline/controller.ts b/src/components-lib/timeline/controller.ts index d37cbad6..e857cdec 100644 --- a/src/components-lib/timeline/controller.ts +++ b/src/components-lib/timeline/controller.ts @@ -408,7 +408,7 @@ export class TimelineController { this._setTargetBarAppropriately(targetTime); } - this._throttledSetViewDuringRangeChange(targetTime, properties); + void this._throttledSetViewDuringRangeChange(targetTime, properties); } } diff --git a/src/components/image-updating-player.ts b/src/components/image-updating-player.ts index d9741090..668f35a4 100644 --- a/src/components/image-updating-player.ts +++ b/src/components/image-updating-player.ts @@ -112,7 +112,7 @@ export class AdvancedCameraCardImageUpdatingPlayer () => dispatchMediaPlayEvent(this), () => dispatchMediaPauseEvent(this), // Clear image load errors on each timer tick so the next render retries - // the — but only for modes where the underlying URL genuinely + // the -- but only for modes where the underlying URL genuinely // changes between ticks (camera/entity snapshots). For mode: url, the // same static URL will fail the same way every time, so clearing the // error just causes a visible flicker (notification → blank → diff --git a/src/components/live/carousel.ts b/src/components/live/carousel.ts index aed4c636..4979aafd 100644 --- a/src/components/live/carousel.ts +++ b/src/components/live/carousel.ts @@ -233,7 +233,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement { private _setViewCameraID(cameraID?: string | null): void { if (cameraID) { - this.viewManagerEpoch?.manager.setViewByParametersWithNewQuery({ + void this.viewManagerEpoch?.manager.setViewByParametersWithNewQuery({ params: { camera: cameraID, }, @@ -319,9 +319,9 @@ export class AdvancedCameraCardLiveCarousel extends LitElement { const controller = this._mediaLoadedInfoSinkController.get()?.mediaPlayerController; // Fire-and-forget; the `volumechange` event drives the re-render. if (controller?.isMuted()) { - controller.unmute(); + void controller.unmute(); } else { - controller?.mute(); + void controller?.mute(); } } @@ -478,7 +478,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement { const selectedCameraIndex = this._getSelectedCameraIndex(); if (this.viewFilterCameraID) { - this._mediaActionsController.setTarget( + void this._mediaActionsController.setTarget( selectedCameraIndex, // Camera in this carousel is only selected if the camera from the // view matches the filtered camera. @@ -486,7 +486,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement { ); } else { // Carousel is not filtered, so the targeted camera is always selected. - this._mediaActionsController.setTarget(selectedCameraIndex, true); + void this._mediaActionsController.setTarget(selectedCameraIndex, true); } this._mediaHeightController.setSelected(selectedCameraIndex); diff --git a/src/components/live/grid.ts b/src/components/live/grid.ts index 6cb3224e..4d584ea5 100644 --- a/src/components/live/grid.ts +++ b/src/components/live/grid.ts @@ -103,7 +103,7 @@ export class AdvancedCameraCardLiveGrid extends LitElement { protected willUpdate(changedProps: PropertyValues): void { if (changedProps.has('viewManagerEpoch') && this._needsGrid()) { - import('../media-grid.js'); + void import('../media-grid.js'); } } diff --git a/src/components/live/index.ts b/src/components/live/index.ts index 15037a86..95f63519 100644 --- a/src/components/live/index.ts +++ b/src/components/live/index.ts @@ -81,7 +81,7 @@ export class AdvancedCameraCardLive extends LitElement { // from a hidden live view. Treat the live view as having no selected // camera unless it is the active view. const view = this.viewManagerEpoch?.manager.getView(); - this._microphoneActionsController.setSelectedCamera( + void this._microphoneActionsController.setSelectedCamera( view?.is('live') ? view.camera ?? null : null, ); } diff --git a/src/components/live/providers/go2rtc/index.ts b/src/components/live/providers/go2rtc/index.ts index 9e19d5e0..fa5ca8a5 100644 --- a/src/components/live/providers/go2rtc/index.ts +++ b/src/components/live/providers/go2rtc/index.ts @@ -140,7 +140,7 @@ export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer // VideoRTC owns the transition: it updates microphoneStream, swaps the // track on the pre-armed transceiver, and validates against stale async // completions before any reconnect fallback. Fire-and-forget is fine. - /* async */ this._player.setMicrophoneStream(this.microphoneStream ?? null); + void this._player.setMicrophoneStream(this.microphoneStream ?? null); } } diff --git a/src/components/live/providers/go2rtc/video-rtc.js b/src/components/live/providers/go2rtc/video-rtc.js index 77cc1a31..517e1675 100644 --- a/src/components/live/providers/go2rtc/video-rtc.js +++ b/src/components/live/providers/go2rtc/video-rtc.js @@ -271,7 +271,7 @@ export class VideoRTC extends HTMLElement { /** * Owns the microphone stream transition end-to-end: updates the property, * extracts the outbound audio track, and swaps it onto the pre-armed audio - * transceiver via `replaceTrack` — no SDP renegotiation, no visible reload. + * transceiver via `replaceTrack` -- no SDP renegotiation, no visible reload. * * Falls back to a full reconnect if `replaceTrack` rejects, but only when * the rejection still describes the current desired state. The transceiver @@ -410,8 +410,8 @@ export class VideoRTC extends HTMLElement { */ disconnectedCallback() { // Synchronous manager-side cleanup by aborting the load's signal. The - // signal's abort listeners — registered by the card-root listener and - // any sinks in the bubble path — fire even though `parentNode` is + // signal's abort listeners -- registered by the card-root listener and + // any sinks in the bubble path -- fire even though `parentNode` is // already null, because abort is plain JS, not DOM-event-bound. this._abortController?.abort(); this._abortController = null; @@ -833,7 +833,7 @@ export class VideoRTC extends HTMLElement { // Always pre-arm a single outbound audio transceiver so the SDP advertises // the slot from the start. With the slot in place, the mic track can be // attached/detached later via `setMicrophoneStream` (replaceTrack) without - // renegotiating — avoiding a visible reload of this cell each time grid + // renegotiating -- avoiding a visible reload of this cell each time grid // selection moves the mic between cameras. // // Pure SDP allocation: the kind-only `addTransceiver('audio', ...)` form diff --git a/src/components/live/providers/webrtc-card.ts b/src/components/live/providers/webrtc-card.ts index 6e28a864..ae7f6c57 100644 --- a/src/components/live/providers/webrtc-card.ts +++ b/src/components/live/providers/webrtc-card.ts @@ -200,7 +200,7 @@ export class AdvancedCameraCardLiveWebRTCCard extends LitElement implements Medi public updated(): void { // Extract the video component after it has been rendered and generate the // media load event. - this.updateComplete.then(() => { + void this.updateComplete.then(() => { this._videoRTC = this.renderRoot?.querySelector('#webrtc') ?? null; const video = this._getVideo(); if (video) { diff --git a/src/components/loading.ts b/src/components/loading.ts index 68a55fed..022fd0b0 100644 --- a/src/components/loading.ts +++ b/src/components/loading.ts @@ -60,7 +60,7 @@ export class AdvancedCameraCardLoading extends LitElement { } private _startEffect(effect: EffectName): void { - this.effectsManager?.startEffect(effect, { fadeIn: false }); + void this.effectsManager?.startEffect(effect, { fadeIn: false }); this._effectName = effect; } diff --git a/src/components/media-filter.ts b/src/components/media-filter.ts index abe8061f..991095a0 100644 --- a/src/components/media-filter.ts +++ b/src/components/media-filter.ts @@ -76,7 +76,7 @@ class AdvancedCameraCardMediaFilter extends ScopedRegistryHost(LitElement) { this.cameraManager, this.foldersManager, ); - this._mediaFilterController.computeMetadataOptions(this.cameraManager); + void this._mediaFilterController.computeMetadataOptions(this.cameraManager); } // The first time the viewManager is set, compute the initial default selections. diff --git a/src/components/submenu/select-button.ts b/src/components/submenu/select-button.ts index 6ad7cdbf..81dc49d3 100644 --- a/src/components/submenu/select-button.ts +++ b/src/components/submenu/select-button.ts @@ -87,7 +87,7 @@ export class AdvancedCameraCardSubmenuSelectButton extends LitElement { } if (!this._optionTitles) { - this._refreshOptionTitles(); + void this._refreshOptionTitles(); } const entityID = this.submenuSelect.entity; diff --git a/src/components/surround.ts b/src/components/surround.ts index 64873f6b..43d1f31b 100644 --- a/src/components/surround.ts +++ b/src/components/surround.ts @@ -66,7 +66,7 @@ export class AdvancedCameraCardSurround extends LitElement { // eslint-disable-next-line @typescript-eslint/no-unused-vars protected willUpdate(_changedProperties: PropertyValues): void { if (this.timelineConfig?.mode && this.timelineConfig.mode !== 'none') { - import('./timeline-core.js'); + void import('./timeline-core.js'); } } diff --git a/src/components/thumbnail/feature/thumbnail.ts b/src/components/thumbnail/feature/thumbnail.ts index 415b4d6d..4923f33d 100644 --- a/src/components/thumbnail/feature/thumbnail.ts +++ b/src/components/thumbnail/feature/thumbnail.ts @@ -62,7 +62,7 @@ export class AdvancedCameraCardThumbnailFeatureThumbnail extends LitElement { this._embedThumbnailTask?.status === TaskStatus.INITIAL && entries.some((entry) => entry.isIntersecting) ) { - this._embedThumbnailTask?.run(); + void this._embedThumbnailTask?.run(); } } diff --git a/src/components/timeline-core.ts b/src/components/timeline-core.ts index 4a1f61a0..0f86bd21 100644 --- a/src/components/timeline-core.ts +++ b/src/components/timeline-core.ts @@ -277,7 +277,7 @@ export class AdvancedCameraCardTimelineCore extends LitElement { this._controller.setView(this.viewManagerEpoch ?? null), ); } else { - this._controller.setView(this.viewManagerEpoch ?? null); + void this._controller.setView(this.viewManagerEpoch ?? null); } } diff --git a/src/components/viewer/carousel.ts b/src/components/viewer/carousel.ts index 3d46e7da..6c163a1b 100644 --- a/src/components/viewer/carousel.ts +++ b/src/components/viewer/carousel.ts @@ -93,7 +93,7 @@ export class AdvancedCameraCardViewerCarousel extends LitElement { (this._selected !== null && this._media?.[this._selected]?.getID()) || null, callback: () => { this._mediaHeightController.recalculate(); - this._seekHandler(); + void this._seekHandler(); }, }); @@ -388,7 +388,7 @@ export class AdvancedCameraCardViewerCarousel extends LitElement { ?.context?.mediaViewer?.seek?.getTime() !== this.viewManagerEpoch?.oldView?.context?.mediaViewer?.seek?.getTime() ) { - this._seekHandler(); + void this._seekHandler(); } } } @@ -397,7 +397,7 @@ export class AdvancedCameraCardViewerCarousel extends LitElement { if (!this._media?.length || this._selected === null) { this._mediaActionsController.unsetTarget(); } else { - this._mediaActionsController.setTarget( + void this._mediaActionsController.setTarget( this._selected, // Camera in this carousel is only selected if the camera from the view // matches the filtered camera. @@ -438,16 +438,16 @@ export class AdvancedCameraCardViewerCarousel extends LitElement { const seekTimeInMedia = selectedMedia.includesTime(seek); this.toggleAttribute('unseekable', !seekTimeInMedia); if (!seekTimeInMedia && !mediaPlayerController.isPaused()) { - mediaPlayerController.pause(); + void mediaPlayerController.pause(); } else if (seekTimeInMedia && mediaPlayerController.isPaused()) { - mediaPlayerController.play(); + void mediaPlayerController.play(); } const seekTime = (await this.cameraManager?.getMediaSeekTime(selectedMedia, seek)) ?? null; if (seekTime !== null) { - mediaPlayerController.seek(seekTime); + void mediaPlayerController.seek(seekTime); } } diff --git a/src/components/viewer/grid.ts b/src/components/viewer/grid.ts index 16704b39..a7a51eec 100644 --- a/src/components/viewer/grid.ts +++ b/src/components/viewer/grid.ts @@ -72,7 +72,7 @@ export class AdvancedCameraCardViewerGrid extends LitElement { protected willUpdate(changedProps: PropertyValues): void { if (changedProps.has('viewManagerEpoch') && this._needsGrid()) { - import('../media-grid.js'); + void import('../media-grid.js'); } } diff --git a/src/components/viewer/provider.ts b/src/components/viewer/provider.ts index 2c5e6974..037f1a25 100644 --- a/src/components/viewer/provider.ts +++ b/src/components/viewer/provider.ts @@ -157,11 +157,11 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi changedProps.has('resolvedMediaCache') || changedProps.has('hass') ) { - this._resolveURL(); + void this._resolveURL(); } if (changedProps.has('viewerConfig') && this.viewerConfig?.zoomable) { - import('../zoomer.js'); + void import('../zoomer.js'); } } @@ -290,7 +290,7 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi .targetID=${mediaID} @click=${() => { if (this.viewerConfig?.snapshot_click_plays_clip) { - this._switchToRelatedClipView(); + void this._switchToRelatedClipView(); } }} >`} diff --git a/src/components/views.ts b/src/components/views.ts index 356ec3fb..3881d456 100644 --- a/src/components/views.ts +++ b/src/components/views.ts @@ -90,16 +90,16 @@ export class AdvancedCameraCardViews extends LitElement { if (changedProps.has('viewManagerEpoch') || changedProps.has('config')) { const view = this.viewManagerEpoch?.manager.getView(); if (view?.is('live') || this._shouldLivePreload()) { - import('./live/index.js'); + void import('./live/index.js'); } if (view?.isGalleryView()) { - import('./gallery/gallery.js'); + void import('./gallery/gallery.js'); } else if (view?.isViewerView()) { - import('./viewer/index.js'); + void import('./viewer/index.js'); } else if (view?.is('image')) { - import('./image.js'); + void import('./image.js'); } else if (view?.is('timeline')) { - import('./timeline.js'); + void import('./timeline.js'); } } diff --git a/src/condition-trigger/common/time-period.ts b/src/condition-trigger/common/time-period.ts index 078e5a9a..210e5cc7 100644 --- a/src/condition-trigger/common/time-period.ts +++ b/src/condition-trigger/common/time-period.ts @@ -6,7 +6,7 @@ import { ConditionState } from '../conditions/types'; // Parses a Home Assistant time-period value (a condition/trigger `for:`) to // seconds, matching HA's `cv.time_period`: // - a number, or a bare numeric string, is a count of seconds; -// - a colon string is `HH:MM` or `HH:MM:SS` — HA reads TWO parts as +// - a colon string is `HH:MM` or `HH:MM:SS` -- HA reads TWO parts as // hours:minutes (not minutes:seconds); // - a `{days, hours, minutes, seconds, milliseconds}` dict (each field a // number or a numeric string, e.g. once a template field has been rendered). diff --git a/src/config/schema/common/string-or-array.ts b/src/config/schema/common/string-or-array.ts index 11b035a4..3213e067 100644 --- a/src/config/schema/common/string-or-array.ts +++ b/src/config/schema/common/string-or-array.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -// A value that may be a single string or a list of strings — common across HA +// A value that may be a single string or a list of strings -- common across HA // condition/trigger fields (e.g. `state`, `to`, `entity_id`). export const stringOrArray = z.string().or(z.string().array()); diff --git a/src/editor.ts b/src/editor.ts index eeaa694e..48cc4f07 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -278,9 +278,9 @@ import { CONF_VIEW_TRIGGERS_ACTIONS_INTERACTION_MODE, CONF_VIEW_TRIGGERS_ACTIONS_TRIGGER, CONF_VIEW_TRIGGERS_ACTIONS_UNTRIGGER, + CONF_VIEW_TRIGGERS_EVENT_HOLD_SECONDS, CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA, CONF_VIEW_TRIGGERS_SHOW_TRIGGER_STATUS, - CONF_VIEW_TRIGGERS_EVENT_HOLD_SECONDS, CONF_VIEW_TRIGGERS_UNTRIGGER_DELAY_SECONDS, CONF_VIEW_TRIGGERS_UNTRIGGER_FORCE_SECONDS, DOCS_URL, @@ -294,7 +294,7 @@ import { HomeAssistant, LovelaceCardEditor } from './ha/types.js'; import { localize } from './localize/localize.js'; import editorStyle from './scss/editor.scss'; import type { CapabilityKey } from './types.js'; -import { arrayMove, prettifyTitle } from './utils/basic.js'; +import { arrayMove, errorToConsole, prettifyTitle } from './utils/basic.js'; import { getCameraID } from './utils/camera.js'; import { fireAdvancedCameraCardEvent } from './utils/fire-advanced-camera-card-event.js'; import { getFolderID } from './utils/folder.js'; @@ -1255,9 +1255,13 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard protected willUpdate(): void { if (!this._initialized) { - sideLoadHomeAssistantElements().then(() => { - this._initialized = true; - }); + sideLoadHomeAssistantElements() + .then(() => { + this._initialized = true; + }) + // A failure leaves the editor with degraded HA form elements and is + // retried on the next update; log it so the cause is at least visible. + .catch((e) => errorToConsole(e)); } } diff --git a/src/ha/web-proxy.ts b/src/ha/web-proxy.ts index 025cf544..1e2c696b 100644 --- a/src/ha/web-proxy.ts +++ b/src/ha/web-proxy.ts @@ -82,7 +82,7 @@ export const createProxiedEndpointIfNecessary = async ( } if (proxyConfig.dynamic) { - // Strip hash fragment — it's client-side only and not relevant for + // Strip hash fragment -- it's client-side only and not relevant for // proxy pattern matching. const url = endpoint.endpoint.split(/#/)[0]; await addDynamicProxyURL(hass, url, { diff --git a/src/patches/ha-camera-stream.ts b/src/patches/ha-camera-stream.ts index d036055c..e80e405e 100644 --- a/src/patches/ha-camera-stream.ts +++ b/src/patches/ha-camera-stream.ts @@ -25,7 +25,7 @@ import { onAbort } from '../utils/abort-signal.js'; import './ha-hls-player.js'; import './ha-web-rtc-player.js'; -customElements.whenDefined('ha-camera-stream').then(() => { +void customElements.whenDefined('ha-camera-stream').then(() => { // ======================================================================================== // From: // - https://github.com/home-assistant/frontend/blob/dev/src/data/camera.ts @@ -56,7 +56,7 @@ customElements.whenDefined('ha-camera-stream').then(() => { // ha-camera-stream renders up to three inner players (MJPEG / HLS / // WebRTC), only one visible. Inner leaves all fire `media:loaded` - // independently — we suppress those at this boundary (`stopPropagation` in + // independently -- we suppress those at this boundary (`stopPropagation` in // `_captureInnerLoad`), cache the latest per type, and republish the // visible one's info via our own source controller in `updated()`. private _mediaLoadedInfoPerStream: Record = {}; diff --git a/src/patches/ha-hls-player.ts b/src/patches/ha-hls-player.ts index 5fb73d75..9d5bd317 100644 --- a/src/patches/ha-hls-player.ts +++ b/src/patches/ha-hls-player.ts @@ -32,7 +32,7 @@ import { } from '../utils/media-info.js'; import { ConstructableLitElement } from './types.js'; -customElements.whenDefined('ha-hls-player').then(() => { +void customElements.whenDefined('ha-hls-player').then(() => { const HaHlsPlayer = customElements.get('ha-hls-player') as ConstructableLitElement; @customElement('advanced-camera-card-ha-hls-player') diff --git a/src/patches/ha-web-rtc-player.ts b/src/patches/ha-web-rtc-player.ts index 3ceed39f..e8ca2b42 100644 --- a/src/patches/ha-web-rtc-player.ts +++ b/src/patches/ha-web-rtc-player.ts @@ -35,7 +35,7 @@ import { } from '../utils/media-info.js'; import { ConstructableLitElement } from './types.js'; -customElements.whenDefined('ha-web-rtc-player').then(() => { +void customElements.whenDefined('ha-web-rtc-player').then(() => { const HaWebRtcPlayer = customElements.get( 'ha-web-rtc-player', ) as ConstructableLitElement; diff --git a/src/scss/live-carousel.scss b/src/scss/live-carousel.scss index b221d917..5dee442f 100644 --- a/src/scss/live-carousel.scss +++ b/src/scss/live-carousel.scss @@ -60,7 +60,7 @@ } } -// Transmitting: breathe between a softer and a fuller glow — never fully fades, +// Transmitting: breathe between a softer and a fuller glow -- never fully fades, // so the camera always reads as live. @keyframes transmitting-vignette-pulse { 0%, diff --git a/src/scss/pop-animation.scss b/src/scss/pop-animation.scss index fff1c6cb..725f7a32 100644 --- a/src/scss/pop-animation.scss +++ b/src/scss/pop-animation.scss @@ -1,7 +1,7 @@ // Shared "pop" enter/exit animation for overlays. // -// `@include pop-in` auto-plays an entrance on render. `@include pop-out` — -// typically guarded by an `.exiting` class — plays the matching exit; it is +// `@include pop-in` auto-plays an entrance on render. `@include pop-out` -- +// typically guarded by an `.exiting` class -- plays the matching exit; it is // named `pop-out` so an `animationend` handler can detect exit completion and // unmount the element. diff --git a/src/types.ts b/src/types.ts index cfc03a3b..84bbaee5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -43,7 +43,7 @@ export interface MediaLoadedInfo { mediaPlayerController?: MediaPlayerController; capabilities?: MediaLoadedCapabilities; - // Universal key identifying "what this media belongs to" — a camera ID for + // Universal key identifying "what this media belongs to" -- a camera ID for // live, a media ID for the viewer, or a sentinel for the image view. targetID?: string; } diff --git a/src/utils/audio.ts b/src/utils/audio.ts index aa0a2608..2db6114f 100644 --- a/src/utils/audio.ts +++ b/src/utils/audio.ts @@ -35,13 +35,13 @@ export const hasAudio = ( pc?: RTCPeerConnection | null, mseCodecs?: string, ): boolean => { - // For WebRTC: Check if there's an audio receiver with an active track. - // We check that the track is not muted because muted means no media data - // is flowing (e.g., the source isn't producing audio). It is not related to - // the audio being muted by the user on the receiving end. - // Only trust receivers when the connection is actually established — a stale - // RTCPeerConnection (e.g. WebRTC failed, fell back to MSE) will have - // receivers with muted tracks that don't reflect actual media availability. + // For WebRTC: Check if there's an audio receiver with an active track. We + // check that the track is not muted because muted means no media data is + // flowing (e.g., the source isn't producing audio). It is not related to the + // audio being muted by the user on the receiving end. Only trust receivers + // when the connection is actually established -- a stale RTCPeerConnection + // (e.g. WebRTC failed, fell back to MSE) will have receivers with muted + // tracks that don't reflect actual media availability. // See: https://github.com/dermotduffy/advanced-camera-card/issues/2417 if (pc && pc.connectionState === 'connected') { const receivers = pc.getReceivers(); diff --git a/src/utils/error-context.ts b/src/utils/error-context.ts index 5cb1ccf5..dd20a623 100644 --- a/src/utils/error-context.ts +++ b/src/utils/error-context.ts @@ -1,7 +1,7 @@ import { AdvancedCameraCardError } from '../types.js'; -// Narrows an unknown error to its structured object `context` — non-null and -// of object type — or null if the error is not an AdvancedCameraCardError or +// Narrows an unknown error to its structured object `context` -- non-null and +// of object type -- or null if the error is not an AdvancedCameraCardError or // has no usable context. Consolidates the instanceof + typeof + null-guard // dance that notification builders and error handlers would otherwise repeat. export const getContextFromError = (error: unknown): object | null => diff --git a/src/utils/event-ancestor.ts b/src/utils/event-ancestor.ts index e9bb7ce3..72d078ef 100644 --- a/src/utils/event-ancestor.ts +++ b/src/utils/event-ancestor.ts @@ -1,7 +1,7 @@ /** * Walk up `element`'s ancestor chain (through shadow boundaries) looking for * an ancestor with the given tag name. Returns true if one is found and that - * same element also appears in the event's composedPath — indicating the event + * same element also appears in the event's composedPath -- indicating the event * originated from within the same subtree as the element. */ export const isAncestorInEventPath = ( diff --git a/src/utils/media-info.ts b/src/utils/media-info.ts index e57cb877..e7382206 100644 --- a/src/utils/media-info.ts +++ b/src/utils/media-info.ts @@ -11,10 +11,10 @@ const MEDIA_INFO_HEIGHT_CUTOFF = 50; const MEDIA_INFO_WIDTH_CUTOFF = MEDIA_INFO_HEIGHT_CUTOFF; /** - * Create a MediaLoadedInfo object. `targetID` is intentionally NOT an option - * — it's owned by the source controller (`MediaLoadedInfoSourceController`) - * and injected at dispatch time, so leaves don't have to (and can't) plumb - * it through info construction. + * Create a MediaLoadedInfo object. `targetID` is intentionally NOT an option -- + * it's owned by the source controller (`MediaLoadedInfoSourceController`) and + * injected at dispatch time, so leaves don't have to (and can't) plumb it + * through info construction. * @param source An event or HTMLElement that should be used as a source. * @returns A new info or null if one could not be created. */ diff --git a/src/utils/sleep.ts b/src/utils/sleep.ts index 8f70f9c0..1c7c4d7f 100644 --- a/src/utils/sleep.ts +++ b/src/utils/sleep.ts @@ -1,4 +1,6 @@ // Usage of this function needs to be justified with a comment. export const sleep = async (seconds: number) => { + // This is the low-level delay primitive callers reach for instead of a raw timer. + // eslint-disable-next-line no-restricted-syntax await new Promise((r) => setTimeout(r, seconds * 1000)); }; diff --git a/src/utils/timer.ts b/src/utils/timer.ts index 9e988828..f4d79dc9 100644 --- a/src/utils/timer.ts +++ b/src/utils/timer.ts @@ -19,6 +19,8 @@ export class Timer { public start(seconds: number, func: () => void): void { this.stop(); + // This class is the sanctioned wrapper for the browser timer APIs. + // eslint-disable-next-line no-restricted-syntax this._timer = window.setTimeout(() => { this._timer = null; func(); @@ -28,6 +30,8 @@ export class Timer { public startRepeated(seconds: number, func: () => void): void { this.stop(); + // This class is the sanctioned wrapper for the browser timer APIs. + // eslint-disable-next-line no-restricted-syntax this._timer = window.setInterval(() => { func(); }, seconds * 1000); diff --git a/src/utils/zod/deep-remove-defaults.ts b/src/utils/zod/deep-remove-defaults.ts index f66466a4..3dec0342 100644 --- a/src/utils/zod/deep-remove-defaults.ts +++ b/src/utils/zod/deep-remove-defaults.ts @@ -76,7 +76,7 @@ function strip(schema: z.ZodType, cache: Map): z.ZodType { let result: z.ZodType; if (schema instanceof z.ZodDefault || schema instanceof z.ZodPrefault) { - // Unwrap the default — don't cache the wrapper itself. + // Unwrap the default -- don't cache the wrapper itself. result = strip(toClassic(schema.unwrap()), cache); } else if (schema instanceof z.ZodObject) { const newShape: Record = {}; diff --git a/src/view/target-id.ts b/src/view/target-id.ts index acadaa95..1c0874a5 100644 --- a/src/view/target-id.ts +++ b/src/view/target-id.ts @@ -6,12 +6,12 @@ import { View } from './view'; // the target-ID namespace. Must not collide with real camera IDs or media IDs. export const IMAGE_VIEW_TARGET_ID_SENTINEL = '__IMAGE_VIEW__'; -// Returns a universal target identifier for the current view — the single key +// Returns a universal target identifier for the current view -- the single key // used by PTZ/zoom state and media retry epochs to identify "what is currently // being displayed." For live, this is the *base* camera ID (substream is an // implementation detail of how to play camera X, not a separate logical -// identity — see `getStreamCameraID` for the substream-aware variant used -// only inside the playback chain). +// identity -- see `getStreamCameraID` for the substream-aware variant used only +// inside the playback chain). export const getViewTargetID = (view: View): string | null => { if (view.isViewerView()) { return view.queryResults?.getSelectedResult()?.getID() ?? null; diff --git a/tests/action-handler-directive.test.ts b/tests/action-handler-directive.test.ts index 6d5aba3e..f7fc207f 100644 --- a/tests/action-handler-directive.test.ts +++ b/tests/action-handler-directive.test.ts @@ -46,7 +46,7 @@ describe('ActionHandler', () => { // A document-level mouseup should cancel the hold timer. document.dispatchEvent(new MouseEvent('mouseup')); - // Advance past hold time — hold should NOT have triggered. + // Advance past hold time -- hold should NOT have triggered. vi.advanceTimersByTime(500); element.dispatchEvent(new MouseEvent('click')); diff --git a/tests/card-controller/actions/actions/ptz.test.ts b/tests/card-controller/actions/actions/ptz.test.ts index 0606f852..9822bcc9 100644 --- a/tests/card-controller/actions/actions/ptz.test.ts +++ b/tests/card-controller/actions/actions/ptz.test.ts @@ -501,7 +501,7 @@ describe('should handle ptz action', () => { // Emulate the stop being called while the action is running, but before // the *next* timer is scheduled. - let resolve: () => void; + let resolve: () => void = () => {}; const promise: Promise = new Promise((_resolve) => { resolve = _resolve; }); @@ -512,7 +512,7 @@ describe('should handle ptz action', () => { action.stop(); - resolve!(); + resolve(); await vi.runOnlyPendingTimersAsync(); // There should be no additional calls. diff --git a/tests/card-controller/call/tones/custom.test.ts b/tests/card-controller/call/tones/custom.test.ts index 18b29646..67074943 100644 --- a/tests/card-controller/call/tones/custom.test.ts +++ b/tests/card-controller/call/tones/custom.test.ts @@ -10,13 +10,13 @@ interface AudioMocks { } // Uses real jsdom HTMLAudioElement instances and only stubs the parts jsdom -// can't fulfil (`play()` / `pause()` — no audio backend). Tests then exercise -// observable behaviour: registered listeners fire via `dispatchEvent`, -// property writes round-trip on the element, etc. +// can't fulfil (`play()` / `pause()` -- no audio backend). Tests then exercise +// observable behaviour: registered listeners fire via `dispatchEvent`, property +// writes round-trip on the element, etc. // // Called once at module load. The `beforeEach`/`afterEach` calls inside this -// helper register Vitest hooks at the file level — Vitest picks them up just -// as if they had been written at the top of the file — so every test in the +// helper register Vitest hooks at the file level -- Vitest picks them up just +// as if they had been written at the top of the file -- so every test in the // file gets fresh mocks installed/torn down automatically. const useAudioElementMocks = (): AudioMocks => { const handle = { instances: [] as HTMLAudioElement[] } as AudioMocks; diff --git a/tests/card-controller/call/tones/test-utils.ts b/tests/card-controller/call/tones/test-utils.ts index c61a37ec..c0d808db 100644 --- a/tests/card-controller/call/tones/test-utils.ts +++ b/tests/card-controller/call/tones/test-utils.ts @@ -5,8 +5,9 @@ import { mock, MockProxy } from 'vitest-mock-extended'; // constructs an AudioContext directly; here we stub the global `AudioContext` // to return a deep-mocked instance whose `createOscillator()` / `createGain()` // factories return a *fresh* mock per call (not a shared one). This lets tests -// assert against individual notes — e.g. `audio.oscillators[2].frequency.value` -// — instead of having every call write over the same observable state. +// assert against individual notes -- e.g. +// `audio.oscillators[2].frequency.value` -- instead of having every call write +// over the same observable state. interface AudioMocks { audioContext: MockProxy; audioContextCtor: Mock<[], MockProxy>; @@ -15,7 +16,7 @@ interface AudioMocks { oscillators: MockProxy[]; gains: MockProxy[]; - // gainParams[i] is the AudioParam exposed by `gains[i].gain` — kept as a + // gainParams[i] is the AudioParam exposed by `gains[i].gain` -- kept as a // parallel array because `mock()` doesn't auto-populate the // AudioParam interface as a callable deep mock (we wire it up by hand). gainParams: MockProxy[]; @@ -26,9 +27,9 @@ interface AudioMocks { // can read its fields after each `beforeEach` runs. // // Called once at module load. The `beforeEach`/`afterEach` calls inside this -// helper register Vitest hooks at the file level — Vitest picks them up just as -// if they had been written at the top of the file — so every test in the file -// gets fresh mocks installed/torn down automatically. +// helper register Vitest hooks at the file level -- Vitest picks them up just +// as if they had been written at the top of the file -- so every test in the +// file gets fresh mocks installed/torn down automatically. export const useAudioMocks = (): AudioMocks => { const audio = {} as AudioMocks; @@ -64,7 +65,7 @@ export const useAudioMocks = (): AudioMocks => { // The source chains `.catch(...)` on the close() Promise. vi.mocked(audio.audioContext.close).mockResolvedValue(); - // The base class reads `_currentTime` from this — left as a deep-mock spy + // The base class reads `_currentTime` from this -- left as a deep-mock spy // by default it'd return a function, so anchor it at 0 for predictable // scheduling assertions. Object.defineProperty(audio.audioContext, 'currentTime', { diff --git a/tests/card-controller/config/overrides-manager.test.ts b/tests/card-controller/config/overrides-manager.test.ts index 178e33c7..f556ea53 100644 --- a/tests/card-controller/config/overrides-manager.test.ts +++ b/tests/card-controller/config/overrides-manager.test.ts @@ -355,7 +355,7 @@ describe('OverridesManager', () => { it('with `invalid_type` surfacing the attempted value', () => { const error = runInvalidOverride((config) => { assert(config.overrides); - // @ts-expect-error — intentionally invalid runtime value to trigger + // @ts-expect-error -- intentionally invalid runtime value to trigger // Zod's `invalid_type` issue code. config.overrides[0].merge = 6; }); diff --git a/tests/card-controller/fullscreen/screenfull/index.test.ts b/tests/card-controller/fullscreen/screenfull/index.test.ts index c461b55d..460e592d 100644 --- a/tests/card-controller/fullscreen/screenfull/index.test.ts +++ b/tests/card-controller/fullscreen/screenfull/index.test.ts @@ -1,7 +1,7 @@ import screenfull from 'screenfull'; -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { ScreenfullFullScreenProvider } from '../../../../src/card-controller/fullscreen/screenfull'; -import { createCardAPI, setScreenfulEnabled } from '../../../test-utils'; +import { createCardAPI, flushPromises, setScreenfulEnabled } from '../../../test-utils'; vi.mock('screenfull', () => ({ default: { @@ -21,6 +21,11 @@ const setScreenfulFullscreen = (fullscreen: boolean): void => { // @vitest-environment jsdom describe('ScreenfullFullScreenProvider', () => { + beforeEach(() => { + vi.mocked(screenfull.request).mockResolvedValue(); + vi.mocked(screenfull.exit).mockResolvedValue(); + }); + afterEach(() => { vi.restoreAllMocks(); }); @@ -158,5 +163,30 @@ describe('ScreenfullFullScreenProvider', () => { expect(screenfull.request).not.toBeCalled(); expect(screenfull.exit).not.toBeCalled(); }); + + it('should swallow a rejected fullscreen request', async () => { + setScreenfulEnabled(true); + const api = createCardAPI(); + const element = document.createElement('div'); + vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element); + vi.mocked(screenfull.request).mockRejectedValue(new Error('denied')); + const provider = new ScreenfullFullScreenProvider(api, vi.fn()); + + provider.setFullscreen(true); + await flushPromises(); + + expect(screenfull.request).toBeCalledWith(element); + }); + + it('should swallow a rejected fullscreen exit', async () => { + setScreenfulEnabled(true); + vi.mocked(screenfull.exit).mockRejectedValue(new Error('not in fullscreen')); + const provider = new ScreenfullFullScreenProvider(createCardAPI(), vi.fn()); + + provider.setFullscreen(false); + await flushPromises(); + + expect(screenfull.exit).toBeCalled(); + }); }); }); diff --git a/tests/card-controller/fullscreen/webkit/index.test.ts b/tests/card-controller/fullscreen/webkit/index.test.ts index c50db267..3303d0b3 100644 --- a/tests/card-controller/fullscreen/webkit/index.test.ts +++ b/tests/card-controller/fullscreen/webkit/index.test.ts @@ -3,7 +3,11 @@ import { mock } from 'vitest-mock-extended'; import { WebkitFullScreenProvider } from '../../../../src/card-controller/fullscreen/webkit'; import { ConditionStateManager } from '../../../../src/condition-trigger/conditions/state-manager'; import { MediaPlayerController, WebkitHTMLVideoElement } from '../../../../src/types'; -import { createCardAPI, createMediaLoadedInfo } from '../../../test-utils'; +import { + createCardAPI, + createMediaLoadedInfo, + flushPromises, +} from '../../../test-utils'; const createWebkitVideoElement = (): HTMLVideoElement & Partial => { @@ -242,7 +246,7 @@ describe('WebkitFullScreenProvider', () => { provider.connect(); const element = createWebkitVideoElement(); - element.play = vi.fn(); + element.play = vi.fn().mockResolvedValue(undefined); const mediaPlayerController = createMediaPlayerController(element); vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue( @@ -263,4 +267,30 @@ describe('WebkitFullScreenProvider', () => { expect(element.play).toBeCalled(); }); + + it('should swallow a rejected video replay after fullscreen ends', async () => { + const api = createCardAPI(); + const stateManager = new ConditionStateManager(); + vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager); + + const provider = new WebkitFullScreenProvider(api, vi.fn()); + provider.connect(); + + const element = createWebkitVideoElement(); + element.play = vi.fn().mockRejectedValue(new Error('denied')); + + const mediaPlayerController = createMediaPlayerController(element); + vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue( + createMediaLoadedInfo({ mediaPlayerController }), + ); + stateManager.setState({ + mediaLoadedInfo: createMediaLoadedInfo({ mediaPlayerController }), + }); + + element.dispatchEvent(new Event('webkitendfullscreen')); + vi.runOnlyPendingTimers(); + await flushPromises(); + + expect(element.play).toBeCalled(); + }); }); diff --git a/tests/card-controller/hass/hass-manager.test.ts b/tests/card-controller/hass/hass-manager.test.ts index ae467694..0e316a53 100644 --- a/tests/card-controller/hass/hass-manager.test.ts +++ b/tests/card-controller/hass/hass-manager.test.ts @@ -145,7 +145,7 @@ describe('HASSManager', () => { startingHASS.config.state = STATE_STARTING; manager.setHASS(startingHASS); - // No reinit yet — HA isn't fully ready. + // No reinit yet -- HA isn't fully ready. expect(api.getInitializationManager().uninitialize).not.toBeCalled(); expect(api.getCameraManager().destroy).not.toBeCalled(); @@ -176,7 +176,7 @@ describe('HASSManager', () => { startingHASS.config.state = STATE_STARTING; manager.setHASS(startingHASS); - // WS came back but integrations still loading — wait for RUNNING. + // WS came back but integrations still loading -- wait for RUNNING. expect(api.getInitializationManager().uninitialize).not.toBeCalled(); expect(api.getCameraManager().destroy).not.toBeCalled(); }); @@ -190,9 +190,9 @@ describe('HASSManager', () => { readyHASS.config.state = STATE_RUNNING; manager.setHASS(readyHASS); - // First-ever hass set — there's no "previous not-ready state" to - // transition from, so the normal first-load init flow applies and we - // must not blow away cameras. + // First-ever hass set -- there's no "previous not-ready state" to + // transition from, so the normal first-load init flow applies and we must + // not blow away cameras. expect(api.getInitializationManager().uninitialize).not.toBeCalled(); expect(api.getCameraManager().destroy).not.toBeCalled(); }); diff --git a/tests/card-controller/issues/issue-manager.test.ts b/tests/card-controller/issues/issue-manager.test.ts index 1a32bc37..ba910878 100644 --- a/tests/card-controller/issues/issue-manager.test.ts +++ b/tests/card-controller/issues/issue-manager.test.ts @@ -198,11 +198,11 @@ describe('IssueManager', () => { const manager = new IssueManager(api); - // hasIssue returns true from the start — simulates trigger() having - // already mutated state before detectDynamic snapshots. The - // before/after check inside detectDynamic sees true→true (no - // transition), but the presence comparison against ConditionState - // must still detect the change. + // hasIssue returns true from the start -- simulates trigger() having + // already mutated state before detectDynamic snapshots. The before/after + // check inside detectDynamic sees true→true (no transition), but the + // presence comparison against ConditionState must still detect the + // change. const description = createIssueDescription(); const issue = createIssue('config_error', { hasIssue: vi.fn().mockReturnValue(true), @@ -219,7 +219,7 @@ describe('IssueManager', () => { expect(api.getCardElementManager().update).toBeCalled(); }); - it('should never auto-popup on trigger — non-full-card issues surface via the status-bar icon; user clicks to open', () => { + it('should never auto-popup on trigger -- non-full-card issues surface via the status-bar icon; user clicks to open', () => { const api = createCardAPI(); vi.mocked(api.getConditionStateManager().getState).mockReturnValue({}); @@ -250,8 +250,8 @@ describe('IssueManager', () => { expect(issue.retry).toBeCalled(); - // Timer should have been reset — advancing less than retrySeconds - // should not fire it again. + // Timer should have been reset -- advancing less than retrySeconds should + // not fire it again. assert(issue.retry); vi.mocked(issue.retry).mockClear(); vi.advanceTimersByTime(500); @@ -312,11 +312,11 @@ describe('IssueManager', () => { }); it('should call update when an active issue swaps sub-states without changing the key set', () => { - // Simulates ConnectionIssue going from 'lost' to 'starting': the - // presence key set ({connection}) is identical, but the description - // value differs. Because IssuePresence is a Map, - // the condition state diff sees the value-level change and fires - // listeners — the IssueManager's own listener calls update(). + // Simulates ConnectionIssue going from 'lost' to 'starting': the presence + // key set ({connection}) is identical, but the description value differs. + // Because IssuePresence is a Map, the condition state + // diff sees the value-level change and fires listeners -- the + // IssueManager's own listener calls update(). const api = createCardAPI(); // Real ConditionStateManager so its isEqual-based diff actually runs. @@ -638,7 +638,7 @@ describe('IssueManager', () => { const { manager, issue } = createRetriableSetup({ retrySeconds: 'auto' }); manager.evaluate(); - // Run two retries — second delay should be 2x the first. + // Run two retries -- second delay should be 2x the first. vi.advanceTimersByTime(RETRY_EXPONENTIAL_BASE_SECONDS * 0.75 * 1000); vi.advanceTimersByTime(RETRY_EXPONENTIAL_BASE_SECONDS * 2 * 0.75 * 1000); expect(issue.retry).toBeCalledTimes(2); @@ -671,16 +671,16 @@ describe('IssueManager', () => { }); manager.evaluate(); - // Three gated firings — each at the base delay (22.5s with 0.75 jitter). - // If the counter were incrementing on gated fires, the second would be - // at 45s and we'd never reach it after only 22.5s. + // Three gated firings -- each at the base delay (22.5s with 0.75 jitter). + // If the counter were incrementing on gated fires, the second would be at + // 45s and we'd never reach it after only 22.5s. vi.advanceTimersByTime(RETRY_EXPONENTIAL_BASE_SECONDS * 0.75 * 1000); vi.advanceTimersByTime(RETRY_EXPONENTIAL_BASE_SECONDS * 0.75 * 1000); vi.advanceTimersByTime(RETRY_EXPONENTIAL_BASE_SECONDS * 0.75 * 1000); expect(issue.retry).not.toBeCalled(); - // Clear the interaction. The next firing — still at the base delay — - // is now allowed and the retry runs. + // Clear the interaction. The next firing -- still at the base delay -- is + // now allowed and the retry runs. vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(false); vi.advanceTimersByTime(RETRY_EXPONENTIAL_BASE_SECONDS * 0.75 * 1000); expect(issue.retry).toBeCalledTimes(1); @@ -854,7 +854,7 @@ describe('IssueManager', () => { vi.mocked(api.getConditionStateManager().getState).mockReturnValue({}); const manager = new IssueManager(api); - // Plain Issue implementation — no optional methods installed. + // Plain Issue implementation -- no optional methods installed. const issue: Issue = { key: 'config_error', hasIssue: () => false, diff --git a/tests/card-controller/issues/issues/media-load.test.ts b/tests/card-controller/issues/issues/media-load.test.ts index 73766b4b..e5f93156 100644 --- a/tests/card-controller/issues/issues/media-load.test.ts +++ b/tests/card-controller/issues/issues/media-load.test.ts @@ -2,9 +2,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { mock } from 'vitest-mock-extended'; import { MediaLoadIssue } from '../../../../src/card-controller/issues/issues/media-load'; import { InternalCallbackActionConfig } from '../../../../src/config/schema/actions/custom/internal'; +import { IMAGE_VIEW_TARGET_ID_SENTINEL } from '../../../../src/view/target-id'; import { View } from '../../../../src/view/view'; import { createCardAPI, createMediaLoadedInfo } from '../../../test-utils'; -import { IMAGE_VIEW_TARGET_ID_SENTINEL } from '../../../../src/view/target-id'; const createAPI = () => createCardAPI(); @@ -133,7 +133,7 @@ describe('MediaLoadIssue', () => { vi.advanceTimersByTime(10000); expect(issue.hasIssue()).toBe(true); - // Same target, different media view — issue stays active. + // Same target, different media view -- issue stays active. issue.detectDynamic({ targetID: 'camera-1', view: 'clip' }); expect(issue.hasIssue()).toBe(true); }); @@ -145,7 +145,7 @@ describe('MediaLoadIssue', () => { vi.advanceTimersByTime(10000); expect(issue.hasIssue()).toBe(true); - // Switch to camera-2 which has no error — should deactivate and start + // Switch to camera-2 which has no error -- should deactivate and start // a fresh timer for the new target. issue.detectDynamic({ targetID: 'camera-2', view: 'live' }); expect(issue.hasIssue()).toBe(false); @@ -164,7 +164,7 @@ describe('MediaLoadIssue', () => { issue.detectDynamic({ targetID: 'camera-1', view: 'live' }); expect(issue.hasIssue()).toBe(true); - // Switch to camera-2 which also has an error — should stay active. + // Switch to camera-2 which also has an error -- should stay active. issue.detectDynamic({ targetID: 'camera-2', view: 'live' }); expect(issue.hasIssue()).toBe(true); }); @@ -456,7 +456,7 @@ describe('MediaLoadIssue', () => { issue.retry(); - // Issue remains active — no new 10s grace period. The error stays + // Issue remains active -- no new 10s grace period. The error stays // visible while the provider re-attempts loading underneath. expect(issue.hasIssue()).toBe(true); }); @@ -542,9 +542,9 @@ describe('MediaLoadIssue', () => { issue.retry(); - // After retry, the issue stays active and the errored target is - // preserved — no new 10s grace period. If media:loaded fires, the - // existing _handleMediaLoaded path will clear everything. + // After retry, the issue stays active and the errored target is preserved + // -- no new 10s grace period. If media:loaded fires, the existing + // _handleMediaLoaded path will clear everything. expect(issue.hasIssue()).toBe(true); issue.detectDynamic({ targetID: 'camera-1', view: 'live' }); expect(issue.hasIssue()).toBe(true); @@ -579,8 +579,8 @@ describe('MediaLoadIssue', () => { // Card detaches: timer must stop. issue.suspend(); - // Full 10s later (plus margin) the timer has NOT matured — the user - // was offscreen and that time does not count against them. + // Full 10s later (plus margin) the timer has NOT matured -- the user was + // offscreen and that time does not count against them. vi.advanceTimersByTime(20000); expect(issue.hasIssue()).toBe(false); expect(onChange).not.toBeCalled(); @@ -594,7 +594,7 @@ describe('MediaLoadIssue', () => { vi.advanceTimersByTime(10000); expect(issue.hasIssue()).toBe(true); - // Card detaches — issue must remain visible on reattach. + // Card detaches -- issue must remain visible on reattach. issue.suspend(); expect(issue.hasIssue()).toBe(true); @@ -609,8 +609,8 @@ describe('MediaLoadIssue', () => { issue.suspend(); // Reattach: the manager's resume() triggers evaluate() → detectDynamic. - // The target is still loading, so the timer arms with a fresh 10s - // window — not whatever was left when we suspended. + // The target is still loading, so the timer arms with a fresh 10s window + // -- not whatever was left when we suspended. issue.detectDynamic({ targetID: 'camera-1', view: 'live' }); vi.advanceTimersByTime(9999); diff --git a/tests/card-controller/issues/state-manager.test.ts b/tests/card-controller/issues/state-manager.test.ts index b279fdd1..254cc317 100644 --- a/tests/card-controller/issues/state-manager.test.ts +++ b/tests/card-controller/issues/state-manager.test.ts @@ -68,6 +68,28 @@ describe('IssueStateManager', () => { expect(mockLegacyResource.detectStatic).toBeCalledWith(hass); expect(mockMediaLoad.detectStatic).toBeCalledWith(hass); }); + + it('should isolate a failing issue and continue detecting the rest', async () => { + const spy = vi.spyOn(console, 'warn').mockReturnValue(); + assert(mockConfigUpgrade.detectStatic); + assert(mockLegacyResource.detectStatic); + assert(mockMediaLoad.detectStatic); + + vi.mocked(mockConfigUpgrade.detectStatic).mockRejectedValue(new Error('boom')); + + // A second failure, a non-Error rejection, is isolated and logged too. + vi.mocked(mockLegacyResource.detectStatic).mockRejectedValue('bad'); + + const manager = createManager(); + const hass = createHASS(); + + await expect(manager.detectStatic(hass)).resolves.toBeUndefined(); + + // Detection continued to the final issue despite the two earlier failures. + expect(mockMediaLoad.detectStatic).toBeCalledWith(hass); + expect(spy).toBeCalledTimes(2); + spy.mockRestore(); + }); }); describe('trigger', () => { diff --git a/tests/card-controller/media-player-manager.test.ts b/tests/card-controller/media-player-manager.test.ts index fb750b5e..27c8a2eb 100644 --- a/tests/card-controller/media-player-manager.test.ts +++ b/tests/card-controller/media-player-manager.test.ts @@ -451,9 +451,9 @@ describe('MediaPlayerManager', () => { const api = createCardAPI(); vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager()); - // Bypass schema validation — the code has a runtime guard (TypeScript - // narrowing) for the case where dashboard config is present but - // dashboard_path / view_path are missing. + // Bypass schema validation -- the code has a runtime guard + // (TypeScript narrowing) for the case where dashboard config is + // present but dashboard_path / view_path are missing. const configWithNoDashboardPaths = createCameraConfig({ camera_entity: 'camera.foo', }); diff --git a/tests/card-controller/view/view-manager.test.ts b/tests/card-controller/view/view-manager.test.ts index c88a48f2..b816e7b0 100644 --- a/tests/card-controller/view/view-manager.test.ts +++ b/tests/card-controller/view/view-manager.test.ts @@ -509,7 +509,7 @@ describe('should handle exceptions', () => { expect.objectContaining({ error }), ); - // The loading flag must be cleared on error — otherwise gallery/viewer + // The loading flag must be cleared on error -- otherwise gallery/viewer // components render "Awaiting media" indefinitely on top of the error // notification. expect(manager.getView()?.context?.loading?.query).toBeUndefined(); @@ -538,9 +538,9 @@ describe('should handle exceptions', () => { viewQueryExecutor: viewQueryExecutor, }); - // Concurrent reset during the await — clears `_view` before the - // rejection is processed. The error path must not crash on the null - // view when attempting to clear the loading flag. + // Concurrent reset during the await -- clears `_view` before the rejection + // is processed. The error path must not crash on the null view when + // attempting to clear the loading flag. viewQueryExecutor.getNewQueryModifiers.mockImplementation(async () => { manager.reset(); throw error; diff --git a/tests/components-lib/live/microphone-actions-controller.test.ts b/tests/components-lib/live/microphone-actions-controller.test.ts index 27593368..14dc754a 100644 --- a/tests/components-lib/live/microphone-actions-controller.test.ts +++ b/tests/components-lib/live/microphone-actions-controller.test.ts @@ -56,6 +56,19 @@ describe('MicrophoneActionsController', () => { expect(microphoneManager.mute).not.toBeCalled(); }); + it('should swallow a rejected auto-unmute so a denied microphone does not surface', async () => { + const microphoneManager = createMicrophoneManager(); + vi.mocked(microphoneManager.unmute).mockRejectedValue(new Error('denied')); + const controller = new MicrophoneActionsController(); + controller.setOptions({ + microphoneManager, + autoUnmuteConditions: ['selected' as const], + }); + + await expect(controller.setSelectedCamera('camera-1')).resolves.toBeUndefined(); + expect(microphoneManager.unmute).toBeCalledTimes(1); + }); + it('should mute on unselected when transitioning to a new camera', async () => { const microphoneManager = createMicrophoneManager(); const controller = new MicrophoneActionsController(); diff --git a/tests/components-lib/media-loaded-info-sink-controller.test.ts b/tests/components-lib/media-loaded-info-sink-controller.test.ts index 68d8699e..b4e080ab 100644 --- a/tests/components-lib/media-loaded-info-sink-controller.test.ts +++ b/tests/components-lib/media-loaded-info-sink-controller.test.ts @@ -61,7 +61,7 @@ describe('MediaLoadedInfoSinkController', () => { // Both cached, but only the selected one is exposed. expect(controller.get()).toBe(infoA); - // Selecting the other target switches what `get()` returns — without a + // Selecting the other target switches what `get()` returns -- without a // new event arriving for it. selected = 'target-B'; controller.hostUpdated(); @@ -102,7 +102,7 @@ describe('MediaLoadedInfoSinkController', () => { controller.hostConnected(); vi.clearAllMocks(); - // Load for an unselected target — cached but inactive. + // Load for an unselected target -- cached but inactive. host.dispatchEvent( createMediaLoadedInfoEvent({ info: createMediaLoadedInfo({ targetID: 'target-B' }), @@ -162,7 +162,7 @@ describe('MediaLoadedInfoSinkController', () => { controller.hostUpdated(); vi.clearAllMocks(); - // Switch to another target with no cached info — active stays null. + // Switch to another target with no cached info -- active stays null. selected = 'target-B'; controller.hostUpdated(); diff --git a/tests/components-lib/media-loaded-info-source-controller.test.ts b/tests/components-lib/media-loaded-info-source-controller.test.ts index 581f098d..0709cff0 100644 --- a/tests/components-lib/media-loaded-info-source-controller.test.ts +++ b/tests/components-lib/media-loaded-info-source-controller.test.ts @@ -126,9 +126,9 @@ describe('MediaLoadedInfoSourceController', () => { }); it('should abort the prior dispatch when targetID changes between calls', () => { - // Without this, the manager would zombie an entry under the old - // targetID — its `onAbort` cleanup never fires because we never aborted - // the prior signal before overwriting `_abort`. + // Without this, the manager would zombie an entry under the old targetID + // -- its `onAbort` cleanup never fires because we never aborted the prior + // signal before overwriting `_abort`. let targetID: string | null = 'target-1'; const host = createLitElement(); const controller = new MediaLoadedInfoSourceController(host, { @@ -164,7 +164,7 @@ describe('MediaLoadedInfoSourceController', () => { controller.set(createMediaLoadedInfo()); const firstSignal = (handler.mock.calls[0][0] as CustomEvent).detail.signal; - // Disconnect and reconnect — without a fresh `set`. + // Disconnect and reconnect -- without a fresh `set`. controller.hostDisconnected(); controller.hostConnected(); @@ -201,7 +201,7 @@ describe('MediaLoadedInfoSourceController', () => { host.addEventListener('advanced-camera-card:media:loaded', handler); controller.set(createMediaLoadedInfo()); - // Active registration, no disconnect — connect should be a no-op. + // Active registration, no disconnect -- connect should be a no-op. controller.hostConnected(); expect(handler).toBeCalledTimes(1); @@ -228,7 +228,7 @@ describe('MediaLoadedInfoSourceController', () => { targetID = 'target-2'; controller.hostConnected(); - // No re-dispatch — the stale cache was discarded. + // No re-dispatch -- the stale cache was discarded. expect(handler).toBeCalledTimes(1); // A subsequent set() under the new target dispatches fresh. diff --git a/tests/components-lib/signed-url-controller.test.ts b/tests/components-lib/signed-url-controller.test.ts index 2376f864..e21db08d 100644 --- a/tests/components-lib/signed-url-controller.test.ts +++ b/tests/components-lib/signed-url-controller.test.ts @@ -771,7 +771,7 @@ describe('SignedURLController', () => { controller.hostUpdate(); expect(createProxiedEndpointIfNecessary).toHaveBeenCalledTimes(1); - // Resolve the original request — should still succeed. + // Resolve the original request -- should still succeed. resolveProxy?.({ endpoint: 'http://proxied-url.com', sign: false }); await flushPromises(); @@ -809,7 +809,7 @@ describe('SignedURLController', () => { expect(createProxiedEndpointIfNecessary).toHaveBeenCalledTimes(1); expect(controller.getValue()).toBe('http://signed.com'); - // Change only the extraneous field — should hit the cache, not re-fetch. + // Change only the extraneous field -- should hit the cache, not re-fetch. extraneous = false; await controller.hostUpdate(); expect(createProxiedEndpointIfNecessary).toHaveBeenCalledTimes(1); diff --git a/tests/components-lib/status-bar-controller.test.ts b/tests/components-lib/status-bar-controller.test.ts index eeb94b09..f9423b09 100644 --- a/tests/components-lib/status-bar-controller.test.ts +++ b/tests/components-lib/status-bar-controller.test.ts @@ -314,12 +314,12 @@ describe('StatusBarController', () => { sufficient: true, }; - // Start with permanent item — bar stays visible. + // Start with permanent item -- bar stays visible. controller.setItems([permanentItem, nonPermanentItem]); vi.advanceTimersByTime(10000); expect(host.getAttribute('hide')).toBe(null); - // Remove permanent item — popup timer starts. + // Remove permanent item -- popup timer starts. controller.setItems([nonPermanentItem]); expect(host.getAttribute('hide')).toBe(null); @@ -342,7 +342,7 @@ describe('StatusBarController', () => { string: 'Title', sufficient: true, }; - // A permanent item that is NOT sufficient — removing it does not + // A permanent item that is NOT sufficient -- removing it does not // change the sufficient-values set, so the popup timer takes the // dedicated permanent-removal branch. const permanentInsufficientItem = { @@ -356,7 +356,7 @@ describe('StatusBarController', () => { vi.advanceTimersByTime(10000); expect(host.getAttribute('hide')).toBe(null); - // Remove the permanent (insufficient) item — sufficient values are + // Remove the permanent (insufficient) item -- sufficient values are // unchanged, but the popup timer must still start. controller.setItems([sufficientItem]); expect(host.getAttribute('hide')).toBe(null); diff --git a/tests/components-lib/thumbnail/feature/controller.test.ts b/tests/components-lib/thumbnail/feature/controller.test.ts index 69ac769c..192d144b 100644 --- a/tests/components-lib/thumbnail/feature/controller.test.ts +++ b/tests/components-lib/thumbnail/feature/controller.test.ts @@ -1,5 +1,5 @@ import { format } from 'date-fns'; -import { describe, expect, it } from 'vitest'; +import { assert, describe, expect, it } from 'vitest'; import { mock } from 'vitest-mock-extended'; import { CameraManager } from '../../../../src/camera-manager/manager'; import { ThumbnailFeatureController } from '../../../../src/components-lib/thumbnail/feature/controller'; @@ -12,6 +12,8 @@ describe('ThumbnailFeatureController', () => { title: 'Test Event', cameraID: 'camera_1', }); + const itemStartTime = itemWithTime.getStartTime(); + assert(itemStartTime); describe('should set title', () => { it('should set title with start time ', () => { @@ -20,7 +22,7 @@ describe('ThumbnailFeatureController', () => { controller.calculate(null, itemWithTime, false); // Use format() to generate expected time in local timezone - const expectedTime = format(itemWithTime.getStartTime()!, 'HH:mm'); + const expectedTime = format(itemStartTime, 'HH:mm'); expect(controller.getTitle()).toBe(expectedTime); }); @@ -54,7 +56,7 @@ describe('ThumbnailFeatureController', () => { controller.calculate(null, itemWithTime, false); // Use format() to generate expected date string (formats in local time) - const expectedDate = format(itemWithTime.getStartTime()!, 'MMM do'); + const expectedDate = format(itemStartTime, 'MMM do'); expect(controller.getSubtitles()).toContain(expectedDate); }); diff --git a/tests/components/notification/no-media.test.ts b/tests/components/notification/no-media.test.ts index 8f964a99..8888e921 100644 --- a/tests/components/notification/no-media.test.ts +++ b/tests/components/notification/no-media.test.ts @@ -61,7 +61,7 @@ describe('renderNoMedia', () => { }); it('should not include metadata when no camera is resolvable', () => { - // Empty store — no default camera. + // Empty store -- no default camera. const cameraManager = createCameraManager(createStore()); const result = renderNoMedia({ diff --git a/tests/view/query-results.test.ts b/tests/view/query-results.test.ts index d2a40954..9a556b14 100644 --- a/tests/view/query-results.test.ts +++ b/tests/view/query-results.test.ts @@ -78,36 +78,37 @@ describe('dispatchViewContextChangeEvent', () => { const results = new QueryResults({ results: testResults }); const slice = results.getSlice('office'); expect(slice).not.toBeNull(); - expect(slice!.getResults()).toEqual( + assert(slice); + expect(slice.getResults()).toEqual( testResults.filter((item) => item.getCameraID() === 'office'), ); - expect(slice!.getResultsCount()).toEqual(100); - expect(slice!.hasResults()).toBeTruthy(); - expect(slice!.getResult(0)).not.toBeNull(); - expect(slice!.getResult()).toBeNull(); - expect(slice!.getSelectedIndex()).toBe(99); - expect(slice!.getSelectedResult()?.getID()).toEqual('id-office-99'); - expect(slice!.hasSelectedResult()).toBeTruthy(); + expect(slice.getResultsCount()).toEqual(100); + expect(slice.hasResults()).toBeTruthy(); + expect(slice.getResult(0)).not.toBeNull(); + expect(slice.getResult()).toBeNull(); + expect(slice.getSelectedIndex()).toBe(99); + expect(slice.getSelectedResult()?.getID()).toEqual('id-office-99'); + expect(slice.hasSelectedResult()).toBeTruthy(); - expect(slice!.resetSelectedResult()); - expect(slice!.getSelectedResult()).toBeNull(); + expect(slice.resetSelectedResult()); + expect(slice.getSelectedResult()).toBeNull(); - expect(slice!.selectIndex(10)); - expect(slice!.getSelectedIndex()).toBe(10); + expect(slice.selectIndex(10)); + expect(slice.getSelectedIndex()).toBe(10); - expect(slice!.selectIndex(10000)); - expect(slice!.getSelectedIndex()).toBe(10); + expect(slice.selectIndex(10000)); + expect(slice.getSelectedIndex()).toBe(10); - expect(slice!.selectIndex(-10000)); - expect(slice!.getSelectedIndex()).toBe(10); + expect(slice.selectIndex(-10000)); + expect(slice.getSelectedIndex()).toBe(10); - slice!.selectResultIfFound((item: ViewItem) => item.getID() === 'id-office-42'); - expect(slice!.getSelectedResult()?.getID()).toBe('id-office-42'); + slice.selectResultIfFound((item: ViewItem) => item.getID() === 'id-office-42'); + expect(slice.getSelectedResult()?.getID()).toBe('id-office-42'); - slice!.selectBestResult((itemArray: ViewItem[]) => + slice.selectBestResult((itemArray: ViewItem[]) => itemArray.findIndex((item) => item.getID() === 'id-office-43'), ); - expect(slice!.getSelectedResult()?.getID()).toBe('id-office-43'); + expect(slice.getSelectedResult()?.getID()).toBe('id-office-43'); }); describe('should respect select approach during construction', () => {