test(ocr): convert useOcrJob polling tests to fake timers

Replaces 2 setTimeout-based wait() helpers with vi.useFakeTimers() +
vi.advanceTimersByTimeAsync() so the polling-loop tests no longer
race against the real clock under CI load — they instead deterministically
advance the setInterval by the exact poll interval and let microtasks
flush. Also converts the destroy() .not.toThrow smoke into a direct
expect(job.destroy()).toBeUndefined() check.

Per Sara: polling-loop tests are the legitimate case for fake timers
(time progression matters) — exactly the pattern she requested.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-11 17:22:58 +02:00
committed by marcel
parent 29672c066b
commit a483c1020f

View File

@@ -1,4 +1,4 @@
import { describe, it, expect, vi, afterEach } from 'vitest';
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
import { createOcrJob } from './useOcrJob.svelte';
afterEach(() => {
@@ -238,8 +238,15 @@ describe('createOcrJob.checkStatus', () => {
});
});
describe('createOcrJob — polling loop (short interval, real timers)', () => {
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
describe('createOcrJob — polling loop (fake timers)', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
// const wait used to live here; replaced by vi.advanceTimersByTimeAsync below.
it('updates progressMessage from translated job code', async () => {
const fetchImpl = makeFetch({
@@ -261,7 +268,7 @@ describe('createOcrJob — polling loop (short interval, real timers)', () => {
pollIntervalMs: 20
});
await job.triggerOcr('KURRENT', false);
await wait(150);
await vi.advanceTimersByTimeAsync(50);
expect(job.progressMessage).not.toBe('');
job.destroy();
@@ -287,7 +294,7 @@ describe('createOcrJob — polling loop (short interval, real timers)', () => {
pollIntervalMs: 20
});
await job.triggerOcr('KURRENT', false);
await wait(150);
await vi.advanceTimersByTimeAsync(50);
expect(job.skippedPages).toBeGreaterThanOrEqual(0);
job.destroy();
@@ -317,7 +324,7 @@ describe('createOcrJob — polling loop (short interval, real timers)', () => {
resetDelayMs: 10
});
await job.triggerOcr('KURRENT', false);
await wait(200);
await vi.advanceTimersByTimeAsync(100);
expect(onJobFinished).toHaveBeenCalledWith('DONE');
job.destroy();
@@ -347,7 +354,7 @@ describe('createOcrJob — polling loop (short interval, real timers)', () => {
resetDelayMs: 10
});
await job.triggerOcr('KURRENT', false);
await wait(200);
await vi.advanceTimersByTimeAsync(100);
expect(onJobFinished).toHaveBeenCalledWith('FAILED');
expect(job.errorMessage).toBeTruthy();
@@ -372,7 +379,7 @@ describe('createOcrJob — polling loop (short interval, real timers)', () => {
pollIntervalMs: 20
});
await job.triggerOcr('KURRENT', false);
await wait(150);
await vi.advanceTimersByTimeAsync(50);
expect(job.running).toBe(true);
job.destroy();
@@ -399,7 +406,7 @@ describe('createOcrJob — polling loop (short interval, real timers)', () => {
pollIntervalMs: 20
});
await job.triggerOcr('KURRENT', false);
await wait(150);
await vi.advanceTimersByTimeAsync(50);
expect(job.running).toBe(true);
job.destroy();
@@ -407,13 +414,14 @@ describe('createOcrJob — polling loop (short interval, real timers)', () => {
});
describe('createOcrJob.destroy', () => {
it('stops polling and is safe to call without an active job', () => {
it('returns undefined and is safe to call without an active job', () => {
const job = createOcrJob({ documentId: () => 'doc-1' });
expect(() => job.destroy()).not.toThrow();
// destroy() is a void function — call it directly. If it threw, the test would fail.
expect(job.destroy()).toBeUndefined();
});
it('stops the polling interval when called mid-poll', async () => {
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
vi.useFakeTimers();
const fetchImpl = vi.fn(async (url: RequestInfo | URL) => {
const u = url.toString();
if (u.includes('/api/documents/doc-1/ocr') && !u.includes('jobs')) {
@@ -437,8 +445,9 @@ describe('createOcrJob.destroy', () => {
job.destroy();
const callsAtDestroy = fetchImpl.mock.calls.length;
await wait(200);
await vi.advanceTimersByTimeAsync(100);
// No additional fetch calls after destroy
expect(fetchImpl.mock.calls.length).toBe(callsAtDestroy);
vi.useRealTimers();
});
});