This commit is contained in:
2025-12-01 17:27:56 +01:00
parent e7ada8aa23
commit 98a09a3f2b
41 changed files with 2341 additions and 1525 deletions

View File

@@ -71,7 +71,7 @@ describe('companion start automation - browser not connected at step 1', () => {
expect(session.state.value).toBe('FAILED');
const error = session.errorMessage as string | undefined;
expect(error).toBeDefined();
expect(error).toContain('Step 1 (LOGIN)');
expect(error).toContain('Step 1 (Navigate to Hosted Racing page)');
expect(error).toContain('Browser not connected');
});
});

View File

@@ -0,0 +1,146 @@
import { describe, it, expect } from 'vitest';
import { MockAutomationLifecycleEmitter } from '../../../mocks/MockAutomationLifecycleEmitter';
import { OverlaySyncService } from 'packages/application/services/OverlaySyncService';
import type { AutomationEvent } from 'packages/application/ports/IAutomationEventPublisher';
import type { OverlayAction } from 'packages/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 as any,
publisher: publisher as any,
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.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 as any,
publisher: publisher as any,
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.actionId).toBe('hosted-failure');
});
});