Add the web interface
Plain HTML, CSS and JavaScript modules, with no build step. Hash routes and relative URLs make it work under any path. The job list is polled: every 2 seconds while a job runs, and less often otherwise.
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
// Tests for the job polling in bindery/static/js/jobs.js. Run with: node --test tests/js/
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import { after, beforeEach, test } from 'node:test';
|
||||
|
||||
// jobs.js listens for tab visibility changes when it loads.
|
||||
globalThis.document = { hidden: false, addEventListener() {} };
|
||||
|
||||
let jobs = [];
|
||||
globalThis.fetch = async () => ({ ok: true, status: 200, json: async () => jobs });
|
||||
|
||||
const { refreshJobs, startPolling, stopPolling, watchJobs } = await import('../../bindery/static/js/jobs.js');
|
||||
|
||||
const job = (id, storyId, status) => ({ id, story_id: storyId, status });
|
||||
const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
beforeEach(async () => {
|
||||
jobs = [];
|
||||
startPolling();
|
||||
await tick();
|
||||
});
|
||||
|
||||
after(() => stopPolling());
|
||||
|
||||
test('watchJobs renders the jobs of one story', async () => {
|
||||
const rendered = [];
|
||||
const stop = watchJobs({ storyId: 1, render: (list) => rendered.push(list.map((j) => j.id)) });
|
||||
jobs = [job('a', 1, 'queued'), job('b', 2, 'queued')];
|
||||
await refreshJobs();
|
||||
assert.deepEqual(rendered.at(-1), ['a']);
|
||||
stop();
|
||||
jobs = [job('c', 1, 'queued')];
|
||||
await refreshJobs();
|
||||
assert.deepEqual(rendered.at(-1), ['a']);
|
||||
});
|
||||
|
||||
test('watchJobs reports a finished job once', async () => {
|
||||
let finished = 0;
|
||||
const stop = watchJobs({ storyId: 1, onFinished: () => finished++ });
|
||||
jobs = [job('a', 1, 'queued')];
|
||||
await refreshJobs();
|
||||
jobs = [job('a', 1, 'running')];
|
||||
await refreshJobs();
|
||||
assert.equal(finished, 0);
|
||||
jobs = [job('a', 1, 'done')];
|
||||
await refreshJobs();
|
||||
assert.equal(finished, 1);
|
||||
await refreshJobs();
|
||||
assert.equal(finished, 1);
|
||||
stop();
|
||||
});
|
||||
|
||||
test('watchJobs ignores jobs that finished before it started, and other stories', async () => {
|
||||
let finished = 0;
|
||||
jobs = [job('old', 1, 'done')];
|
||||
await refreshJobs();
|
||||
const stop = watchJobs({ storyId: 1, onFinished: () => finished++ });
|
||||
jobs = [job('old', 1, 'done'), job('other', 2, 'running')];
|
||||
await refreshJobs();
|
||||
jobs = [job('old', 1, 'done'), job('other', 2, 'error')];
|
||||
await refreshJobs();
|
||||
assert.equal(finished, 0);
|
||||
stop();
|
||||
});
|
||||
|
||||
test('watchJobs without a story watches every job', async () => {
|
||||
let finished = 0;
|
||||
const stop = watchJobs({ onFinished: () => finished++ });
|
||||
jobs = [job('a', 1, 'running'), job('b', null, 'queued')];
|
||||
await refreshJobs();
|
||||
jobs = [job('a', 1, 'cancelled'), job('b', null, 'error')];
|
||||
await refreshJobs();
|
||||
assert.equal(finished, 1);
|
||||
stop();
|
||||
});
|
||||
|
||||
test('stopPolling stops the requests', async () => {
|
||||
stopPolling();
|
||||
let calls = 0;
|
||||
const saved = globalThis.fetch;
|
||||
globalThis.fetch = async () => {
|
||||
calls++;
|
||||
return { ok: true, status: 200, json: async () => [] };
|
||||
};
|
||||
await refreshJobs();
|
||||
assert.equal(calls, 0);
|
||||
globalThis.fetch = saved;
|
||||
});
|
||||
Reference in New Issue
Block a user