All checks were successful
Build, test and publish / verify (push) Successful in 7m6s
109 lines
3.9 KiB
TypeScript
109 lines
3.9 KiB
TypeScript
import { TestBed } from '@angular/core/testing';
|
|
import { SchedulingRun, SchedulingState, SchedulingStatus } from './scheduling-status';
|
|
|
|
class FakeEventSource {
|
|
static current: FakeEventSource;
|
|
readonly close = vi.fn();
|
|
onmessage?: (event: { data: string }) => void;
|
|
onerror?: () => void;
|
|
constructor(readonly url: string) {
|
|
FakeEventSource.current = this;
|
|
}
|
|
send(runs: SchedulingRun[] | null): void {
|
|
this.onmessage?.({ data: JSON.stringify({ runs }) });
|
|
}
|
|
}
|
|
|
|
function run(state: SchedulingState, runId = 'run-1'): SchedulingRun {
|
|
return {
|
|
runId,
|
|
schoolId: 1,
|
|
showId: 2,
|
|
showName: 'Zomershow',
|
|
startedAt: '2026-11-03T12:00:00Z',
|
|
finishedAt: state === 'RUNNING' ? null : '2026-11-03T12:01:02Z',
|
|
state,
|
|
progress: { phase: 'VARIETY', feasible: true },
|
|
message: 'Afgerond',
|
|
summary: null,
|
|
};
|
|
}
|
|
|
|
describe('Scheduling status over SSE', () => {
|
|
let status: SchedulingStatus;
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
vi.stubGlobal('EventSource', FakeEventSource);
|
|
TestBed.configureTestingModule({});
|
|
status = TestBed.inject(SchedulingStatus);
|
|
status.connect();
|
|
});
|
|
afterEach(() => {
|
|
TestBed.resetTestingModule();
|
|
vi.unstubAllGlobals();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('recovers an existing outcome without replaying old notifications', () => {
|
|
expect(FakeEventSource.current.url).toBe('/api/scheduling/events');
|
|
FakeEventSource.current.send([run('OPTIMAL')]);
|
|
expect(status.run(1, 2)?.state).toBe('OPTIMAL');
|
|
expect(status.notifications()).toEqual([]);
|
|
expect(status.elapsed(run('OPTIMAL'))).toBe('1m 2s');
|
|
expect(status.phase(run('OPTIMAL'))).toBe('Variatie');
|
|
});
|
|
|
|
it('announces a completion missed during disconnection exactly once', () => {
|
|
FakeEventSource.current.send([run('RUNNING')]);
|
|
FakeEventSource.current.onerror?.();
|
|
expect(status.connected()).toBe(false);
|
|
const disconnected = FakeEventSource.current;
|
|
vi.advanceTimersByTime(3000);
|
|
expect(FakeEventSource.current).not.toBe(disconnected);
|
|
expect(disconnected.close).toHaveBeenCalledOnce();
|
|
expect(status.run(1, 2)?.state).toBe('RUNNING');
|
|
FakeEventSource.current.send([run('VALID')]);
|
|
expect(status.connected()).toBe(true);
|
|
expect(status.notifications()).toHaveLength(1);
|
|
status.dismiss();
|
|
FakeEventSource.current.send([run('VALID')]);
|
|
expect(status.notifications()).toEqual([]);
|
|
});
|
|
|
|
it('does not treat heartbeats or phase changes as completions', () => {
|
|
FakeEventSource.current.send(null);
|
|
FakeEventSource.current.send([run('OPTIMAL')]);
|
|
expect(status.notifications()).toEqual([]);
|
|
FakeEventSource.current.send([run('RUNNING', 'run-2')]);
|
|
FakeEventSource.current.send(null);
|
|
expect(status.run(1, 2)?.runId).toBe('run-2');
|
|
expect(status.notifications()).toEqual([]);
|
|
FakeEventSource.current.send([run('SOLVER_FAILED', 'run-2')]);
|
|
expect(status.notifications()).toHaveLength(1);
|
|
});
|
|
|
|
it('announces a locally started run completed before the first SSE snapshot', () => {
|
|
FakeEventSource.current.onerror?.();
|
|
status.started(run('RUNNING'));
|
|
vi.advanceTimersByTime(3000);
|
|
FakeEventSource.current.send([run('OPTIMAL'), run('VALID', 'historical')]);
|
|
expect(status.notifications().map((item) => item.runId)).toEqual(['run-1']);
|
|
status.dismiss();
|
|
FakeEventSource.current.onerror?.();
|
|
vi.advanceTimersByTime(3000);
|
|
FakeEventSource.current.send([run('OPTIMAL'), run('VALID', 'historical')]);
|
|
expect(status.notifications()).toEqual([]);
|
|
});
|
|
|
|
it('retains a fast completion when the start response arrives after SSE', () => {
|
|
FakeEventSource.current.send([]);
|
|
FakeEventSource.current.send([run('OPTIMAL')]);
|
|
status.started(run('RUNNING'));
|
|
expect(status.run(1, 2)?.state).toBe('OPTIMAL');
|
|
status.connect();
|
|
const source = FakeEventSource.current;
|
|
TestBed.resetTestingModule();
|
|
expect(source.close).toHaveBeenCalledOnce();
|
|
});
|
|
});
|