diff --git a/src/card-controller/initialization/initialization-manager.ts b/src/card-controller/initialization/initialization-manager.ts index 67d0fcff..62703039 100644 --- a/src/card-controller/initialization/initialization-manager.ts +++ b/src/card-controller/initialization/initialization-manager.ts @@ -6,7 +6,7 @@ import { loadLanguages } from '../../localize/localize'; import { errorToConsole } from '../../utils/basic'; import { Initializer } from '../../utils/initializer/initializer'; import type { CardInitializerAPI } from '../types'; -import { SessionManager } from './session-manager'; +import { SessionManager, SessionState } from './session-manager'; export enum InitializationAspect { LANGUAGES = 'languages', @@ -90,11 +90,9 @@ export class InitializationManager { // HASSManager); a reconnect or a cleared issue reaches it by causing a // render. // - // The check here is only to keep cost down: a card that has finished - // initializing re-renders often, and without it each of those renders would - // queue an attempt that does nothing. `_initializeMandatory()` checks the - // same conditions again when it actually runs, and that is the one that - // matters for correctness. + // The check here is a filter rather than the decision: a card that has + // finished initializing re-renders often, and without it each of those + // renders would queue an attempt that does nothing. public triggerInitialization(): void { if (!this._shouldInitializeMandatory()) { return; @@ -107,7 +105,15 @@ export class InitializationManager { this._api.getConfigManager().hasConfig() && this._api.getCardElementManager().isConnected() && isHassReady(this._api.getHASSManager().getHASS()) && - !this.areMandatoryAspectsInitialized() && + // Start when aspects remain to be initialized, or when the session is + // idle even though aspects are otherwise initialized. A run that + // initialized every aspect and then declined (e.g. because something + // ended its session), reports no outcome at all, so only a later run can + // report the card as actually started, and that run will find nothing + // left to initialize. + // See: https://github.com/dermotduffy/advanced-camera-card/issues/2672 + (!this.areMandatoryAspectsInitialized() || + this._sessionManager.getState() === SessionState.IDLE) && // Don't start while a full-card issue (e.g. the "Home Assistant is // starting" notice) is shown: each initialization step aborts as soon as // it sees one, so an attempt now would be wasted. The card tries again diff --git a/tests/card-controller/initialization/initialization-manager.test.ts b/tests/card-controller/initialization/initialization-manager.test.ts index 7ecbd78e..6997af83 100644 --- a/tests/card-controller/initialization/initialization-manager.test.ts +++ b/tests/card-controller/initialization/initialization-manager.test.ts @@ -214,6 +214,42 @@ describe('InitializationManager', () => { expect(manager.getSessionManager().wasEverInitialized()).toBeFalsy(); }); + it('should start the card after a run that initialized every aspect declined to report an outcome', async () => { + // See: https://github.com/dermotduffy/advanced-camera-card/issues/2672 + const api = createReadyAPI(); + const stateManager = new ConditionStateManager(); + vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager); + + const manager = new InitializationManager(api); + + // Home Assistant stops being ready midway through, which ends the session + // without invalidating any aspect. The run carries on to initialize + // everything, then declines because its session is gone. + vi.mocked(api.getViewManager().initialize).mockImplementation(async () => { + manager.getSessionManager().end(); + return true; + }); + + await manager.initializeMandatory(); + + expect(manager.areMandatoryAspectsInitialized()).toBeTruthy(); + expect(manager.getSessionManager().wasEverInitialized()).toBeFalsy(); + + // A render or a hass change reaches the card afterwards. + manager.triggerInitialization(); + + await vi.waitFor(() => + expect(manager.getSessionManager().wasEverInitialized()).toBeTruthy(), + ); + expect(stateManager.getState().initialized).toBe(true); + expect(api.getAutomationsManager().subscribe).toHaveBeenCalled(); + + // The later run found every aspect already initialized. + expect(api.getCameraManager().initializeCamerasFromConfig).toHaveBeenCalledTimes( + 1, + ); + }); + it('should stop when a full-card issue appears during initialization', async () => { const api = createReadyAPI(); @@ -682,10 +718,15 @@ describe('InitializationManager', () => { expect(initializer.initializeMultipleIfNecessary).not.toHaveBeenCalled(); }); - it('should not initialize when already initialized', () => { + it('should not initialize when already initialized and the session is running', () => { const initializer = mock(); initializer.isInitializedMultiple.mockReturnValue(true); const manager = new InitializationManager(createReadyAPI(), initializer); + const sessionManager = manager.getSessionManager(); + sessionManager.reportInitializationSucceeded( + sessionManager.startInitialization(), + createConfig(), + ); manager.triggerInitialization();