Syntax previewClick to edit
12345
export default function pollUntil(check, options = {}) {
// TODO: poll until check returns a truthy value or rejects on timeout/abort
throw new Error('Not implemented');
}
Syntax previewClick to edit
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
import pollUntil from './pollUntil';
function createFakeClock() {
let now = 0;
let nextId = 1;
const timers = new Map();
return {
setTimeout(fn, delay = 0) {
const id = nextId++;
timers.set(id, { at: now + Math.max(0, Number(delay) || 0), fn });
return id;
},
clearTimeout(id) {
timers.delete(id);
},
count() {
return timers.size;
},
async advance(ms) {
const target = now + ms;
await Promise.resolve();
while (true) {
let selectedId;
let selected;
for (const [id, timer] of timers) {
if (timer.at <= target && (!selected || timer.at < selected.at)) {
selectedId = id;
selected = timer;
}
}
if (!selected) break;
now = selected.at;
timers.delete(selectedId);
selected.fn();
await Promise.resolve();
await Promise.resolve();
}
now = target;
await Promise.resolve();
}
};
}
async function withFakeTimers(run) {
const clock = createFakeClock();
const originalSetTimeout = globalThis.setTimeout;
const originalClearTimeout = globalThis.clearTimeout;
globalThis.setTimeout = clock.setTimeout;
globalThis.clearTimeout = clock.clearTimeout;
try {
await run(clock);
} finally {
globalThis.setTimeout = originalSetTimeout;
globalThis.clearTimeout = originalClearTimeout;
}
}
function createSignal() {
const listeners = new Set();
const signal = {
aborted: false,
reason: undefined,
addEventListener(type, listener) { if (type === 'abort') listeners.add(listener); },
removeEventListener(type, listener) { if (type === 'abort') listeners.delete(listener); }
};
return {
signal,
listenerCount: () => listeners.size,
abort(reason) {
if (signal.aborted) return;
signal.aborted = true;
signal.reason = reason;
for (const listener of [...listeners]) listener();
}
};
}
describe('pollUntil', () => {
test('resolves when check becomes truthy', async () => withFakeTimers(async (clock) => {
let count = 0;
const outcome = pollUntil(() => ++count >= 3 ? 'ready' : false, { interval: 5, timeout: 50 });
await clock.advance(10);
expect(await outcome).toBe('ready');
expect(clock.count()).toBe(0);
}));
test('lets a synchronous truthy first check win when timeout is zero', async () => withFakeTimers(async (clock) => {
const outcome = pollUntil(() => 'ready', { timeout: 0 });
await clock.advance(0);
expect(await outcome).toBe('ready');
expect(clock.count()).toBe(0);
}));
test('times out a falsy first check when timeout is zero', async () => withFakeTimers(async (clock) => {
const outcome = pollUntil(() => false, { timeout: 0 }).then(() => null, (error) => error);
await clock.advance(0);
expect((await outcome).message).toBe('Timeout');
expect(clock.count()).toBe(0);
}));
test('times out even when check never settles', async () => withFakeTimers(async (clock) => {
const outcome = pollUntil(() => new Promise(() => {}), { timeout: 20 }).then(
() => null,
(error) => error
);
await clock.advance(20);
expect((await outcome).message).toBe('Timeout');
expect(clock.count()).toBe(0);
}));
test('does not wait a full interval past the deadline', async () => withFakeTimers(async (clock) => {
let calls = 0;
const outcome = pollUntil(() => { calls += 1; return false; }, { interval: 100, timeout: 10 }).then(
() => null,
(error) => error
);
await clock.advance(10);
expect((await outcome).message).toBe('Timeout');
expect(calls).toBe(1);
expect(clock.count()).toBe(0);
}));
test('does not schedule after abort during an in-flight check', async () => withFakeTimers(async (clock) => {
let finishCheck;
const control = createSignal();
const outcome = pollUntil(() => new Promise((resolve) => { finishCheck = resolve; }), {
interval: 10,
signal: control.signal
}).then(() => null, (error) => error);
control.abort();
expect((await outcome).name).toBe('AbortError');
finishCheck(false);
await clock.advance(100);
expect(clock.count()).toBe(0);
expect(control.listenerCount()).toBe(0);
}));
test('skips check when the signal is already aborted', async () => {
let calls = 0;
const control = createSignal();
control.abort();
const outcome = pollUntil(() => { calls += 1; return true; }, { signal: control.signal }).then(
() => null,
(error) => error
);
expect((await outcome).name).toBe('AbortError');
expect(calls).toBe(0);
expect(control.listenerCount()).toBe(0);
});
test('cleans deadline and listener after async rejection', async () => withFakeTimers(async (clock) => {
const control = createSignal();
const failure = new Error('async');
const outcome = pollUntil(() => Promise.reject(failure), { timeout: 50, signal: control.signal }).then(
() => null,
(error) => error
);
await clock.advance(0);
expect(await outcome).toBe(failure);
expect(clock.count()).toBe(0);
expect(control.listenerCount()).toBe(0);
}));
test('propagates synchronous throws and asynchronous rejections', async () => {
const syncError = new Error('sync');
const asyncError = new Error('async');
await expect(pollUntil(() => { throw syncError; })).rejects.toBe(syncError);
await expect(pollUntil(() => Promise.reject(asyncError))).rejects.toBe(asyncError);
});
test('cleans timers and the abort listener after success', async () => withFakeTimers(async (clock) => {
const control = createSignal();
await expect(pollUntil(() => 'done', { timeout: 50, signal: control.signal })).resolves.toBe('done');
expect(clock.count()).toBe(0);
expect(control.listenerCount()).toBe(0);
}));
});
Run tests to see results.