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,79 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { install, server, nodes, find, field, emit, settle } from './flow-helpers.mjs';
|
||||
|
||||
const ui = install();
|
||||
const { api, setUnauthorizedHandler } = await import('../../bindery/static/js/lib.js');
|
||||
const { stopPolling } = await import('../../bindery/static/js/jobs.js');
|
||||
|
||||
test('login retry, navigation, session expiry and logout preserve the authentication lifecycle', async (t) => {
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||||
const backend = server(t);
|
||||
let auth = { authenticated: false, mode: 'cwa', user: null };
|
||||
backend.on('GET', 'auth/check', () => auth);
|
||||
backend.on('GET', 'config', { metadata_fields: {}, mode_labels: {}, library_available: false });
|
||||
backend.on('GET', 'jobs', []);
|
||||
backend.on('GET', 'stories', []);
|
||||
backend.on('GET', 'rules', { rules: [], paragraphs: false });
|
||||
backend.on('GET', 'settings', { recent_chapters: 3, personal_ini: '' });
|
||||
backend.on('GET', 'stories/1', { error: 'Story unavailable' }, 404);
|
||||
backend.on('GET', 'stories/1/rules', { rules: [], paragraphs: null });
|
||||
backend.on('GET', 'chapters/2/diff?', { error: 'Chapter unavailable' }, 404);
|
||||
backend.on('GET', 'expired', { error: 'Session ended' }, 401);
|
||||
backend.on('POST', 'auth/logout', () => { auth = { ...auth, authenticated: false }; return {}; });
|
||||
t.after(() => { stopPolling(); setUnauthorizedHandler(() => {}); });
|
||||
await import('../../bindery/static/js/main.js');
|
||||
await settle();
|
||||
assert.equal(document.activeElement, field(ui.app, 'username'));
|
||||
assert.equal(backend.sent('GET', 'config').length, 0);
|
||||
ui.windowEvents.hashchange();
|
||||
assert.equal(backend.sent('GET', 'stories').length, 0);
|
||||
const form = () => nodes(ui.app).find((el) => el.tagName === 'form');
|
||||
field(ui.app, 'username').value = 'reader';
|
||||
field(ui.app, 'password').value = 'wrong';
|
||||
nodes(form()).find((el) => el.attributes.type === 'checkbox').checked = false;
|
||||
backend.on('POST', 'auth/login', { error: 'Wrong password' }, 401);
|
||||
await emit(form(), 'submit');
|
||||
assert.match(ui.app.textContent, /Wrong password/);
|
||||
assert.equal(find(ui.app, 'button', 'Log in').disabled, false);
|
||||
assert.deepEqual(backend.sent('POST', 'auth/login')[0].body, { username: 'reader', password: 'wrong', remember: false });
|
||||
field(ui.app, 'password').value = 'correct';
|
||||
backend.on('POST', 'auth/login', () => { auth = { authenticated: true, mode: 'cwa', user: 'reader' }; return {}; });
|
||||
await emit(form(), 'submit');
|
||||
await settle();
|
||||
assert.ok(find(ui.app, 'button', 'Log out'));
|
||||
assert.equal(backend.sent('GET', 'config').length, 1);
|
||||
for (const path of ['', '#/settings', '#/story/1', '#/story/1/chapter/2', '#/missing']) {
|
||||
window.location.hash = path;
|
||||
ui.windowEvents.hashchange();
|
||||
await settle();
|
||||
}
|
||||
assert.match(ui.app.textContent, /Page not found/);
|
||||
assert.equal(backend.sent('GET', 'stories/1/rules').length, 1);
|
||||
assert.equal(backend.sent('GET', 'chapters/2/diff?').length, 1);
|
||||
auth = { authenticated: false, mode: 'cwa' };
|
||||
await assert.rejects(api('expired'), /Session ended/);
|
||||
await settle();
|
||||
assert.ok(find(ui.app, 'button', 'Log in'));
|
||||
const polls = backend.sent('GET', 'jobs').length;
|
||||
t.mock.timers.tick(60000);
|
||||
await settle();
|
||||
assert.equal(backend.sent('GET', 'jobs').length, polls);
|
||||
const checks = backend.sent('GET', 'auth/check').length;
|
||||
await assert.rejects(api('expired'), /Session ended/);
|
||||
assert.equal(backend.sent('GET', 'auth/check').length, checks);
|
||||
await emit(form(), 'submit');
|
||||
await settle();
|
||||
await emit(find(ui.app, 'button', 'Log out'), 'click');
|
||||
await settle();
|
||||
assert.ok(find(ui.app, 'button', 'Log in'));
|
||||
assert.equal(backend.sent('POST', 'auth/logout').length, 1);
|
||||
backend.on('POST', 'auth/login', () => {
|
||||
auth = { authenticated: true, mode: 'proxy', user: 'reader', logout_url: 'https://login.example/logout' };
|
||||
return {};
|
||||
});
|
||||
await emit(form(), 'submit');
|
||||
await settle();
|
||||
await emit(find(ui.app, 'button', 'Log out'), 'click');
|
||||
assert.equal(window.location.href, 'https://login.example/logout');
|
||||
});
|
||||
Reference in New Issue
Block a user