275 lines
9.8 KiB
TypeScript
275 lines
9.8 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import {
|
|
SessionManager,
|
|
SessionState,
|
|
} from '../../../src/card-controller/initialization/session-manager';
|
|
import { ConditionStateManager } from '../../../src/condition-trigger/conditions/state-manager';
|
|
import { createTriggerEvaluator } from '../../../src/condition-trigger/triggers/factory';
|
|
import { initializedTriggerSchema } from '../../../src/config/schema/condition-trigger/triggers/custom/initialized';
|
|
import { createTriggerEvaluatorContext } from '../../condition-trigger/triggers/triggers/test-utils';
|
|
import { createConfig } from '../../config/test-utils';
|
|
import { createCardAPI } from '../../test-utils';
|
|
|
|
const createSessionManager = (): {
|
|
sessionManager: SessionManager;
|
|
stateManager: ConditionStateManager;
|
|
} => {
|
|
const api = createCardAPI();
|
|
const stateManager = new ConditionStateManager();
|
|
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
|
|
|
|
return { sessionManager: new SessionManager(api), stateManager };
|
|
};
|
|
|
|
// Take a session through a successful initialization run, leaving the card
|
|
// started (RUNNING).
|
|
const completeInitialization = (
|
|
sessionManager: SessionManager,
|
|
config = createConfig(),
|
|
): void => {
|
|
sessionManager.reportInitializationSucceeded(
|
|
sessionManager.startInitialization(),
|
|
config,
|
|
);
|
|
};
|
|
|
|
describe('SessionManager', () => {
|
|
it('should start idle with nothing published', () => {
|
|
const { sessionManager, stateManager } = createSessionManager();
|
|
|
|
expect(sessionManager.getState()).toBe(SessionState.IDLE);
|
|
expect(sessionManager.wasEverInitialized()).toBeFalsy();
|
|
expect(stateManager.getState().initialized).toBeUndefined();
|
|
});
|
|
|
|
describe('should start initializations', () => {
|
|
it('should start the first initialization of a session', () => {
|
|
const { sessionManager } = createSessionManager();
|
|
|
|
const token = sessionManager.startInitialization();
|
|
|
|
expect(sessionManager.getState()).toBe(SessionState.INITIALIZING);
|
|
expect(sessionManager.isCurrentInitialization(token)).toBeTruthy();
|
|
});
|
|
|
|
it('should keep a started card while an aspect is initialized again', () => {
|
|
const { sessionManager } = createSessionManager();
|
|
completeInitialization(sessionManager);
|
|
|
|
sessionManager.startInitialization();
|
|
|
|
expect(sessionManager.getState()).toBe(SessionState.RUNNING);
|
|
});
|
|
});
|
|
|
|
describe('should report a successful initialization', () => {
|
|
it('should publish the session and config in one change', () => {
|
|
const { sessionManager, stateManager } = createSessionManager();
|
|
const listener = vi.fn();
|
|
stateManager.addListener(listener);
|
|
const config = createConfig();
|
|
|
|
sessionManager.reportInitializationSucceeded(
|
|
sessionManager.startInitialization(),
|
|
config,
|
|
);
|
|
|
|
expect(sessionManager.getState()).toBe(SessionState.RUNNING);
|
|
expect(sessionManager.wasEverInitialized()).toBeTruthy();
|
|
expect(listener).toHaveBeenCalledTimes(1);
|
|
expect(listener).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
change: { config: config, initialized: true, everInitialized: true },
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should republish only the config on a later initialization', () => {
|
|
const { sessionManager, stateManager } = createSessionManager();
|
|
completeInitialization(sessionManager);
|
|
|
|
const listener = vi.fn();
|
|
stateManager.addListener(listener);
|
|
const newConfig = createConfig({ menu: { style: 'none' } });
|
|
sessionManager.reportInitializationSucceeded(
|
|
sessionManager.startInitialization(),
|
|
newConfig,
|
|
);
|
|
|
|
expect(listener).toHaveBeenCalledTimes(1);
|
|
expect(listener).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
change: { config: newConfig },
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should publish nothing on a later initialization with an unchanged config', () => {
|
|
const { sessionManager, stateManager } = createSessionManager();
|
|
completeInitialization(sessionManager);
|
|
|
|
const listener = vi.fn();
|
|
stateManager.addListener(listener);
|
|
sessionManager.reportInitializationSucceeded(
|
|
sessionManager.startInitialization(),
|
|
createConfig(),
|
|
);
|
|
|
|
expect(listener).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('should decline an initialization', () => {
|
|
it('should return to idle before the card has started', () => {
|
|
const { sessionManager, stateManager } = createSessionManager();
|
|
const listener = vi.fn();
|
|
stateManager.addListener(listener);
|
|
|
|
sessionManager.reportInitializationDeclined(sessionManager.startInitialization());
|
|
|
|
expect(sessionManager.getState()).toBe(SessionState.IDLE);
|
|
expect(listener).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should keep a started card', () => {
|
|
const { sessionManager, stateManager } = createSessionManager();
|
|
completeInitialization(sessionManager);
|
|
|
|
const listener = vi.fn();
|
|
stateManager.addListener(listener);
|
|
sessionManager.reportInitializationDeclined(sessionManager.startInitialization());
|
|
|
|
expect(sessionManager.getState()).toBe(SessionState.RUNNING);
|
|
expect(stateManager.getState().initialized).toBe(true);
|
|
expect(listener).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('should fail an initialization', () => {
|
|
it('should return to idle before the card has started', () => {
|
|
const { sessionManager, stateManager } = createSessionManager();
|
|
const listener = vi.fn();
|
|
stateManager.addListener(listener);
|
|
|
|
sessionManager.reportInitializationFailed(sessionManager.startInitialization());
|
|
|
|
expect(sessionManager.getState()).toBe(SessionState.IDLE);
|
|
expect(listener).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should end a started card', () => {
|
|
const { sessionManager, stateManager } = createSessionManager();
|
|
completeInitialization(sessionManager);
|
|
|
|
sessionManager.reportInitializationFailed(sessionManager.startInitialization());
|
|
|
|
expect(sessionManager.getState()).toBe(SessionState.IDLE);
|
|
expect(stateManager.getState().initialized).toBe(false);
|
|
|
|
// A card that has been turndown has still been "ever initialized".
|
|
expect(stateManager.getState().everInitialized).toBe(true);
|
|
expect(sessionManager.wasEverInitialized()).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe('should end a session', () => {
|
|
it('should return to idle after an ended sessiond', () => {
|
|
const { sessionManager, stateManager } = createSessionManager();
|
|
completeInitialization(sessionManager);
|
|
|
|
sessionManager.end();
|
|
|
|
expect(sessionManager.getState()).toBe(SessionState.IDLE);
|
|
expect(stateManager.getState().initialized).toBe(false);
|
|
});
|
|
|
|
it('should write nothing before the card has started', () => {
|
|
const { sessionManager, stateManager } = createSessionManager();
|
|
const listener = vi.fn();
|
|
stateManager.addListener(listener);
|
|
|
|
sessionManager.startInitialization();
|
|
sessionManager.end();
|
|
|
|
expect(sessionManager.getState()).toBe(SessionState.IDLE);
|
|
expect(listener).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should leave the card reported as ever initialized', () => {
|
|
const { sessionManager, stateManager } = createSessionManager();
|
|
completeInitialization(sessionManager);
|
|
|
|
sessionManager.end();
|
|
|
|
expect(stateManager.getState().everInitialized).toBe(true);
|
|
expect(sessionManager.wasEverInitialized()).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe('should refuse stale tokens', () => {
|
|
it('should refuse a token from before the session ended', () => {
|
|
const { sessionManager, stateManager } = createSessionManager();
|
|
const listener = vi.fn();
|
|
stateManager.addListener(listener);
|
|
|
|
const token = sessionManager.startInitialization();
|
|
sessionManager.end();
|
|
|
|
expect(sessionManager.isCurrentInitialization(token)).toBeFalsy();
|
|
|
|
sessionManager.reportInitializationSucceeded(token, createConfig());
|
|
sessionManager.reportInitializationDeclined(token);
|
|
sessionManager.reportInitializationFailed(token);
|
|
|
|
expect(sessionManager.getState()).toBe(SessionState.IDLE);
|
|
expect(sessionManager.wasEverInitialized()).toBeFalsy();
|
|
expect(listener).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should refuse a token that was already used', () => {
|
|
const { sessionManager, stateManager } = createSessionManager();
|
|
const listener = vi.fn();
|
|
stateManager.addListener(listener);
|
|
|
|
const config = createConfig();
|
|
const token = sessionManager.startInitialization();
|
|
sessionManager.reportInitializationSucceeded(token, config);
|
|
sessionManager.reportInitializationSucceeded(
|
|
token,
|
|
createConfig({ menu: { style: 'none' } }),
|
|
);
|
|
|
|
expect(listener).toHaveBeenCalledTimes(1);
|
|
expect(stateManager.getState().config).toBe(config);
|
|
});
|
|
});
|
|
|
|
// The user-facing behavior the machine exists for, driven through the real
|
|
// schema, trigger factory and evaluator rather than hand-written state.
|
|
describe('should drive the initialized trigger', () => {
|
|
it('should fire once per session and not when a session ends', () => {
|
|
const { sessionManager, stateManager } = createSessionManager();
|
|
|
|
const trigger = initializedTriggerSchema.parse({ trigger: 'initialized' });
|
|
const evaluator = createTriggerEvaluator(
|
|
trigger,
|
|
createTriggerEvaluatorContext({ stateManager }),
|
|
);
|
|
const callback = vi.fn();
|
|
evaluator.subscribe(callback);
|
|
|
|
completeInitialization(sessionManager);
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
|
|
// The session ending is a true -> false change of the watched value, but
|
|
// must not fire the trigger.
|
|
sessionManager.end();
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
|
|
completeInitialization(sessionManager);
|
|
expect(callback).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|
|
});
|