Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -6,7 +6,10 @@ const response = (line: string) => new Response(JSON.stringify({ ok: true, lines
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (error: Error) => void;
|
||||
const promise = new Promise<T>((done, fail) => { resolve = done; reject = fail; });
|
||||
const promise = new Promise<T>((done, fail) => {
|
||||
resolve = done;
|
||||
reject = fail;
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
let documentStub: EventTarget & { visibilityState: string };
|
||||
@@ -16,12 +19,18 @@ beforeEach(() => {
|
||||
documentStub = Object.assign(new EventTarget(), { visibilityState: 'visible' });
|
||||
vi.stubGlobal('document', documentStub);
|
||||
});
|
||||
afterEach(() => { controller?.dispose(); vi.useRealTimers(); vi.unstubAllGlobals(); });
|
||||
afterEach(() => {
|
||||
controller?.dispose();
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
function setup() {
|
||||
const fetcher = vi.fn<typeof fetch>();
|
||||
const onReset = vi.fn();
|
||||
const onData = vi.fn(); const onError = vi.fn(); const onLoading = vi.fn();
|
||||
const onData = vi.fn();
|
||||
const onError = vi.fn();
|
||||
const onLoading = vi.fn();
|
||||
controller = createLogRefresh({ fetcher, onReset, onData, onError, onLoading });
|
||||
return { fetcher, onReset, onData, onError, onLoading };
|
||||
}
|
||||
@@ -32,11 +41,22 @@ const visibility = (state: string) => {
|
||||
|
||||
describe('log refresh controls', () => {
|
||||
it('retains a manually loaded log through same-server fleet updates and resets only on server navigation', async () => {
|
||||
const fetcher = vi.fn<typeof fetch>().mockImplementation(async () => response('loaded log'));
|
||||
const fetcher = vi
|
||||
.fn<typeof fetch>()
|
||||
.mockImplementation(async () => response('loaded log'));
|
||||
let displayed: string[] = [];
|
||||
const reset = vi.fn(() => { displayed = []; });
|
||||
controller = createLogRefresh({ fetcher, onReset: reset,
|
||||
onData: (_key, body) => { displayed = body.lines ?? []; }, onError: vi.fn(), onLoading: vi.fn() });
|
||||
const reset = vi.fn(() => {
|
||||
displayed = [];
|
||||
});
|
||||
controller = createLogRefresh({
|
||||
fetcher,
|
||||
onReset: reset,
|
||||
onData: (_key, body) => {
|
||||
displayed = body.lines ?? [];
|
||||
},
|
||||
onError: vi.fn(),
|
||||
onLoading: vi.fn()
|
||||
});
|
||||
controller.configure(target, true, 5000);
|
||||
await controller.refresh();
|
||||
controller.configure(target, false, 5000);
|
||||
@@ -68,7 +88,8 @@ describe('log refresh controls', () => {
|
||||
await controller.refresh();
|
||||
controller.configure(target, false, 5000);
|
||||
await vi.advanceTimersByTimeAsync(30_000);
|
||||
visibility('hidden'); visibility('visible');
|
||||
visibility('hidden');
|
||||
visibility('visible');
|
||||
await vi.advanceTimersByTimeAsync(30_000);
|
||||
expect(fetcher).toHaveBeenCalledTimes(1);
|
||||
expect(onData).toHaveBeenCalledTimes(1);
|
||||
@@ -77,7 +98,8 @@ describe('log refresh controls', () => {
|
||||
|
||||
it('aborts a pending refresh immediately and ignores its late data and loading updates after resume', async () => {
|
||||
const { fetcher, onData, onLoading, onError } = setup();
|
||||
const old = deferred<Response>(); const fresh = deferred<Response>();
|
||||
const old = deferred<Response>();
|
||||
const fresh = deferred<Response>();
|
||||
fetcher.mockReturnValueOnce(old.promise).mockReturnValueOnce(fresh.promise);
|
||||
controller.configure(target, true, 5000);
|
||||
const pending = controller.refresh();
|
||||
@@ -89,7 +111,9 @@ describe('log refresh controls', () => {
|
||||
controller.configure(target, true, 5000);
|
||||
const resumed = controller.refresh();
|
||||
expect(fetcher).toHaveBeenCalledTimes(2);
|
||||
onData.mockClear(); onError.mockClear(); onLoading.mockClear();
|
||||
onData.mockClear();
|
||||
onError.mockClear();
|
||||
onLoading.mockClear();
|
||||
// Simulate a transport that delivers a response despite cancellation.
|
||||
old.resolve(response('obsolete'));
|
||||
await pending;
|
||||
@@ -98,7 +122,10 @@ describe('log refresh controls', () => {
|
||||
expect(onLoading).not.toHaveBeenCalled();
|
||||
fresh.resolve(response('current'));
|
||||
await resumed;
|
||||
expect(onData).toHaveBeenCalledWith('console', expect.objectContaining({ lines: ['current'] }));
|
||||
expect(onData).toHaveBeenCalledWith(
|
||||
'console',
|
||||
expect.objectContaining({ lines: ['current'] })
|
||||
);
|
||||
expect(onLoading).toHaveBeenLastCalledWith(false);
|
||||
});
|
||||
|
||||
@@ -115,20 +142,28 @@ describe('log refresh controls', () => {
|
||||
next.reject(new Error('cancelled response'));
|
||||
await pending;
|
||||
expect(onData).toHaveBeenCalledTimes(1);
|
||||
expect(onData).toHaveBeenLastCalledWith('console', expect.objectContaining({ lines: ['saved'] }));
|
||||
expect(onData).toHaveBeenLastCalledWith(
|
||||
'console',
|
||||
expect.objectContaining({ lines: ['saved'] })
|
||||
);
|
||||
expect(onError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('allows a manual refresh while paused without restarting automatic polling', async () => {
|
||||
const { fetcher, onData } = setup();
|
||||
fetcher.mockResolvedValueOnce(response('initial')).mockResolvedValueOnce(response('manual'));
|
||||
fetcher
|
||||
.mockResolvedValueOnce(response('initial'))
|
||||
.mockResolvedValueOnce(response('manual'));
|
||||
controller.configure(target, true, 5000);
|
||||
await controller.refresh();
|
||||
controller.configure(target, false, 5000);
|
||||
await controller.refresh();
|
||||
await vi.advanceTimersByTimeAsync(30_000);
|
||||
expect(fetcher).toHaveBeenCalledTimes(2);
|
||||
expect(onData).toHaveBeenLastCalledWith('console', expect.objectContaining({ lines: ['manual'] }));
|
||||
expect(onData).toHaveBeenLastCalledWith(
|
||||
'console',
|
||||
expect.objectContaining({ lines: ['manual'] })
|
||||
);
|
||||
});
|
||||
|
||||
it('loads a selected log while paused and never accepts a reply from the prior server or tab', async () => {
|
||||
@@ -146,7 +181,10 @@ describe('log refresh controls', () => {
|
||||
expect(fetcher).toHaveBeenCalledTimes(2);
|
||||
expect(fetcher.mock.calls[1][0]).toContain('/another-server/logs/games_zm?lines=1000');
|
||||
expect(onData).toHaveBeenCalledTimes(1);
|
||||
expect(onData).toHaveBeenLastCalledWith('games_zm', expect.objectContaining({ lines: ['selected log'] }));
|
||||
expect(onData).toHaveBeenLastCalledWith(
|
||||
'games_zm',
|
||||
expect.objectContaining({ lines: ['selected log'] })
|
||||
);
|
||||
});
|
||||
|
||||
it('resumes one polling loop and coalesces manual and scheduled reads', async () => {
|
||||
@@ -173,7 +211,9 @@ describe('log refresh controls', () => {
|
||||
await Promise.resolve();
|
||||
controller.dispose();
|
||||
expect(fetcher.mock.calls[0][1]!.signal!.aborted).toBe(true);
|
||||
onData.mockClear(); onError.mockClear(); onLoading.mockClear();
|
||||
onData.mockClear();
|
||||
onError.mockClear();
|
||||
onLoading.mockClear();
|
||||
body.resolve({ ok: true, lines: ['late body'] });
|
||||
await pending;
|
||||
visibility('visible');
|
||||
@@ -187,7 +227,8 @@ describe('log refresh controls', () => {
|
||||
|
||||
it('keeps the last tail on a non-JSON gateway failure and can recover with a retry', async () => {
|
||||
const { fetcher, onData, onError } = setup();
|
||||
fetcher.mockResolvedValueOnce(response('saved'))
|
||||
fetcher
|
||||
.mockResolvedValueOnce(response('saved'))
|
||||
.mockResolvedValueOnce(new Response('<html>Bad Gateway</html>', { status: 502 }))
|
||||
.mockResolvedValueOnce(response('recovered'));
|
||||
controller.configure(target, false, 5000);
|
||||
@@ -195,10 +236,16 @@ it('keeps the last tail on a non-JSON gateway failure and can recover with a ret
|
||||
await controller.refresh();
|
||||
expect(onError).toHaveBeenLastCalledWith('HTTP 502: Expected a JSON log response.');
|
||||
expect(onData).toHaveBeenCalledTimes(1);
|
||||
expect(onData).toHaveBeenLastCalledWith('console', expect.objectContaining({ lines: ['saved'] }));
|
||||
expect(onData).toHaveBeenLastCalledWith(
|
||||
'console',
|
||||
expect.objectContaining({ lines: ['saved'] })
|
||||
);
|
||||
await controller.refresh();
|
||||
expect(onError).toHaveBeenLastCalledWith('');
|
||||
expect(onData).toHaveBeenLastCalledWith('console', expect.objectContaining({ lines: ['recovered'] }));
|
||||
expect(onData).toHaveBeenLastCalledWith(
|
||||
'console',
|
||||
expect.objectContaining({ lines: ['recovered'] })
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects malformed successful log payloads before they reach the renderer', async () => {
|
||||
|
||||
Reference in New Issue
Block a user