S12 - Async feedback for scheduling
All checks were successful
Build, test and publish / verify (push) Successful in 7m25s
All checks were successful
Build, test and publish / verify (push) Successful in 7m25s
This commit is contained in:
95
agiledancers-fe/src/app/core/scheduling-status.spec.ts
Normal file
95
agiledancers-fe/src/app/core/scheduling-status.spec.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { SchedulingRun, 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: string, 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('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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user