/* eslint-env jest */ import { POLL_MS, WAIT_MS, waitForJob, type FixJobState } from '../fix-job'; /** A clock that moves only when the waiting sleeps. */ const clock = (): { now: () => number; sleep: (ms: number) => Promise; slept: number[]; } => { let time = 1000; const slept: number[] = []; return { now: () => time, sleep: async ms => { slept.push(ms); time += ms; }, slept, }; }; describe('waiting for a fix job', () => { it('asks every few seconds until the answer has come', async () => { const states: FixJobState[] = [ { status: 'running', seconds: 3 }, { status: 'running', seconds: 6 }, { status: 'done', preview: 'the preview' }, ]; const paths: string[] = []; const time = clock(); const answer = await waitForJob('abc123', { ask: async path => { paths.push(path); return states.shift() as FixJobState; }, ...time, }); expect(answer).toBe('the preview'); expect(time.slept).toEqual([POLL_MS, POLL_MS, POLL_MS]); expect(paths[0]).toMatch( /^\/recomaze\/v1\/seo\/post-fix\/job\/abc123\?_=\d+$/ ); expect(new Set(paths).size).toBe(3); }); it("throws a failed job's error as the dialogs read one", async () => { await expect( waitForJob('abc123', { ask: async () => ({ status: 'failed', error: { code: 'rest_forbidden', message: 'Not enough credits for this suggestion.', data: { status: 402 }, }, }), ...clock(), }) ).rejects.toMatchObject({ message: 'Not enough credits for this suggestion.', code: 'rest_forbidden', data: { status: 402 }, }); }); it('gives up after seven minutes without an answer', async () => { const time = clock(); await expect( waitForJob('abc123', { ask: async () => ({ status: 'running' }), ...time, }) ).rejects.toMatchObject({ code: 'recomaze_seo_job_slow', data: { status: 504 }, }); expect(time.slept.length).toBe(Math.floor(WAIT_MS / POLL_MS) + 1); }); it('stops asking once the dialog is closed', async () => { const ask = jest.fn(); await expect( waitForJob('abc123', { ask, cancelled: () => true, ...clock() }) ).rejects.toMatchObject({ code: 'cancelled' }); expect(ask).not.toHaveBeenCalled(); }); });