146 lines
4.4 KiB
TypeScript
146 lines
4.4 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { MockAutomationLifecycleEmitter } from '../../../mocks/MockAutomationLifecycleEmitter';
|
|
import { OverlaySyncService } from 'packages/automation/application/services/OverlaySyncService';
|
|
import type { AutomationEvent } from 'packages/automation/application/ports/IAutomationEventPublisher';
|
|
import type { OverlayAction } from 'packages/automation/application/ports/IOverlaySyncPort';
|
|
|
|
type RendererOverlayState =
|
|
| { status: 'idle' }
|
|
| { status: 'starting'; actionId: string }
|
|
| { status: 'in-progress'; actionId: string }
|
|
| { status: 'completed'; actionId: string }
|
|
| { status: 'failed'; actionId: string };
|
|
|
|
class RecordingPublisher {
|
|
public events: AutomationEvent[] = [];
|
|
async publish(event: AutomationEvent): Promise<void> {
|
|
this.events.push(event);
|
|
}
|
|
}
|
|
|
|
function reduceEventsToRendererState(events: AutomationEvent[]): RendererOverlayState {
|
|
let state: RendererOverlayState = { status: 'idle' };
|
|
|
|
for (const ev of events) {
|
|
if (!ev.actionId) continue;
|
|
switch (ev.type) {
|
|
case 'modal-opened':
|
|
case 'panel-attached':
|
|
state = { status: 'starting', actionId: ev.actionId };
|
|
break;
|
|
case 'action-started':
|
|
state = { status: 'in-progress', actionId: ev.actionId };
|
|
break;
|
|
case 'action-complete':
|
|
state = { status: 'completed', actionId: ev.actionId };
|
|
break;
|
|
case 'action-failed':
|
|
case 'panel-missing':
|
|
state = { status: 'failed', actionId: ev.actionId };
|
|
break;
|
|
}
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
describe('renderer overlay lifecycle integration', () => {
|
|
it('tracks starting → in-progress → completed lifecycle for a hosted action', async () => {
|
|
const emitter = new MockAutomationLifecycleEmitter();
|
|
const publisher = new RecordingPublisher();
|
|
const svc = new OverlaySyncService({
|
|
lifecycleEmitter: emitter,
|
|
publisher,
|
|
logger: console as any,
|
|
defaultTimeoutMs: 2_000,
|
|
});
|
|
|
|
const action: OverlayAction = {
|
|
id: 'hosted-session',
|
|
label: 'Starting hosted session',
|
|
};
|
|
|
|
const ackPromise = svc.startAction(action);
|
|
|
|
expect(publisher.events[0]?.type).toBe('modal-opened');
|
|
expect(publisher.events[0]?.actionId).toBe('hosted-session');
|
|
|
|
await emitter.emit({
|
|
type: 'panel-attached',
|
|
actionId: 'hosted-session',
|
|
timestamp: Date.now(),
|
|
payload: { selector: '#gridpilot-overlay' },
|
|
});
|
|
|
|
await emitter.emit({
|
|
type: 'action-started',
|
|
actionId: 'hosted-session',
|
|
timestamp: Date.now(),
|
|
});
|
|
|
|
const ack = await ackPromise;
|
|
expect(ack.id).toBe('hosted-session');
|
|
expect(ack.status).toBe('confirmed');
|
|
|
|
await publisher.publish({
|
|
type: 'panel-attached',
|
|
actionId: 'hosted-session',
|
|
timestamp: Date.now(),
|
|
payload: { selector: '#gridpilot-overlay' },
|
|
} as AutomationEvent);
|
|
|
|
await publisher.publish({
|
|
type: 'action-started',
|
|
actionId: 'hosted-session',
|
|
timestamp: Date.now(),
|
|
} as AutomationEvent);
|
|
|
|
await publisher.publish({
|
|
type: 'action-complete',
|
|
actionId: 'hosted-session',
|
|
timestamp: Date.now(),
|
|
} as AutomationEvent);
|
|
|
|
const rendererState = reduceEventsToRendererState(publisher.events);
|
|
|
|
expect(rendererState.status).toBe('completed');
|
|
expect((rendererState as { actionId: string }).actionId).toBe('hosted-session');
|
|
});
|
|
|
|
it('ends in failed state when panel-missing is emitted', async () => {
|
|
const emitter = new MockAutomationLifecycleEmitter();
|
|
const publisher = new RecordingPublisher();
|
|
const svc = new OverlaySyncService({
|
|
lifecycleEmitter: emitter,
|
|
publisher,
|
|
logger: console as any,
|
|
defaultTimeoutMs: 200,
|
|
});
|
|
|
|
const action: OverlayAction = {
|
|
id: 'hosted-failure',
|
|
label: 'Hosted session failing',
|
|
};
|
|
|
|
void svc.startAction(action);
|
|
|
|
await publisher.publish({
|
|
type: 'panel-attached',
|
|
actionId: 'hosted-failure',
|
|
timestamp: Date.now(),
|
|
payload: { selector: '#gridpilot-overlay' },
|
|
} as AutomationEvent);
|
|
|
|
await publisher.publish({
|
|
type: 'action-failed',
|
|
actionId: 'hosted-failure',
|
|
timestamp: Date.now(),
|
|
payload: { reason: 'validation error' },
|
|
} as AutomationEvent);
|
|
|
|
const rendererState = reduceEventsToRendererState(publisher.events);
|
|
|
|
expect(rendererState.status).toBe('failed');
|
|
expect((rendererState as { actionId: string }).actionId).toBe('hosted-failure');
|
|
});
|
|
}); |