" } });
assert.ok(el.innerHTML.includes('/pool/a.one'), "shows the warning from the result");
assert.ok(el.innerHTML.includes('not <thawed>
'), "shows the result's warning escaped");
assert.ok(el.innerHTML.includes('class="alert alert-success') && el.innerHTML.includes('succeeded')
&& el.innerHTML.includes(''), "an enabled cancel button");
click9({ target: { dataset: { cancel: "q" } } });
assert.ok(el9.innerHTML.includes('data-cancel="q" disabled>'), "disabled while the request runs");
answer("/c/q", { status: 400, responseJSON: { detail: "This job cannot be cancelled once it has started" } }, false);
assert.ok(el9.innerHTML.includes("This job cannot be cancelled once it has started")
&& el9.innerHTML.includes('class="alert alert-danger'), "the server's reason shows");
assert.ok(open.some((r) => r.url === "/r" && !r.settled), "the list is asked again at once");
answer("/r", { ...empty, jobs: [{ ...queued, state: "running", cancellable: false }] });
assert.ok(el9.innerHTML.includes("cannot be cancelled") && !el9.innerHTML.includes("data-cancel"),
"the reason stays after the refresh");
click9({ target: { dataset: { dismissError: "1" } } });
assert.ok(!el9.innerHTML.includes("cannot be cancelled"), "the reason can be dismissed");
advance(3000);
answer("/r", { ...empty, jobs: [queued] });
click9({ target: { dataset: { cancel: "q" } } });
answer("/c/q", { status: 0 }, false);
assert.ok(el9.innerHTML.includes("could not be reached"), "a network error shows");
// track(id): the panel follows a job the page just started and asks for the list at once
open = []; timers = [];
window.backgroundJobRunning = false;
const el10 = { innerHTML: "", addEventListener() {} };
const panel = jobPanel(el10, urls, "tok", undefined, STATES, TEXT);
advance(0);
answer("/r", empty);
assert.strictEqual(window.backgroundJobRunning, false);
panel.track("t");
assert.strictEqual(window.backgroundJobRunning, true, "a tracked job holds the page's reload");
assert.ok(open.some((r) => r.url === "/r" && !r.settled), "the list is asked for at once");
answer("/r", empty);
answer("/j/t", { ...running, id: "t", label: "Upload", state: "cancelled" });
assert.ok(el10.innerHTML.includes("Upload") && el10.innerHTML.includes('cancelled'),
"the tracked job's outcome shows");
// a list answer older than the job, then its detail still active: the panel keeps following it
open = []; timers = [];
const el11 = { innerHTML: "", addEventListener() {} };
const panel11 = jobPanel(el11, urls, "tok", undefined, STATES, TEXT);
advance(0);
answer("/r", empty);
panel11.track("u");
answer("/r", empty);
answer("/j/u", { ...running, id: "u", label: "Upload" });
assert.ok(!open.some((r) => !r.settled && r.url === "/j/u"), "an active detail is not asked for again at once");
assert.strictEqual(window.backgroundJobRunning, true, "still followed");
advance(3000);
answer("/r", empty);
assert.strictEqual(open.filter((r) => !r.settled && r.url === "/j/u").length, 1, "asked again once per poll");
window.reloaded = false;
answer("/j/u", { ...running, id: "u", label: "Upload", state: "succeeded" });
assert.ok(window.reloaded, "a job that succeeds without a warning reloads the page: it shows the job's effect");
assert.ok(!el11.innerHTML.includes('succeeded'), "not shown as well");
// a job that made a VM opens its page; a warning keeps the outcome, with Open; only a path of this site
open = []; timers = []; window.reloaded = false; window.opened = null;
const elv = { innerHTML: "", addEventListener() {} };
jobPanel(elv, urls, "tok", ["v"], STATES, TEXT);
advance(0);
answer("/r", empty);
answer("/j/v", { ...running, id: "v", label: "Clone VM", state: "succeeded", result: { open: "/instances/9/" } });
assert.ok(window.opened === "/instances/9/" && !window.reloaded, "opens the clone's page");
for (const [result, shown] of [[{ open: "/instances/9/", warning: "not started" }, true],
[{ open: "//evil.example.com/" }, false], [{ open: "javascript:alert(1)" }, false]]) {
open = []; timers = []; window.reloaded = false; window.opened = null;
const elw = { innerHTML: "", addEventListener() {} };
jobPanel(elw, urls, "tok", ["w"], STATES, TEXT);
advance(0);
answer("/r", empty);
answer("/j/w", { ...running, id: "w", state: "succeeded", result });
assert.strictEqual(window.opened, null, JSON.stringify(result));
assert.strictEqual(elw.innerHTML.includes('href="/instances/9/">Open'), shown, JSON.stringify(result));
}
// a dialog open or a field in use: the outcome waits in the panel, with Refresh
for (const [label, doc] of [["dialog", { querySelector: (s) => s === ".modal.show" ? {} : null }],
["field", { activeElement: { matches: (s) => s === "input, select, textarea" } }]]) {
open = []; timers = []; window.reloaded = false;
Object.assign(document, { activeElement: null, querySelector: () => null }, doc);
const elm = { innerHTML: "", addEventListener() {} };
jobPanel(elm, urls, "tok", ["m"], STATES, TEXT);
advance(0);
answer("/r", empty);
answer("/j/m", { ...running, id: "m", label: "Clone", state: "succeeded" });
assert.ok(!window.reloaded && elm.innerHTML.includes('succeeded')
&& elm.innerHTML.includes("data-refresh"), "with a " + label + ": the outcome with Refresh, no reload");
}
Object.assign(document, { activeElement: null, querySelector: () => null });
// an XML edit not saved: no reload, as for the page's status poll
open = []; timers = []; window.reloaded = false;
window.xmlEditUnsaved = () => true;
const elx = { innerHTML: "", addEventListener() {} };
jobPanel(elx, urls, "tok", ["x"], STATES, TEXT);
advance(0);
answer("/r", empty);
answer("/j/x", { ...running, id: "x", state: "succeeded" });
assert.ok(!window.reloaded && elx.innerHTML.includes("data-refresh"), "an unsaved XML edit keeps the page");
delete window.xmlEditUnsaved;
// another job's outcome shown or still awaited: a quiet success does not wipe it
open = []; timers = []; window.reloaded = false;
const elo = { innerHTML: "", addEventListener() {} };
jobPanel(elo, urls, "tok", ["f", "s", "w"], STATES, TEXT);
advance(0);
answer("/r", empty);
answer("/j/f", { ...running, id: "f", label: "Migrate", state: "failed", error: "no route" });
answer("/j/s", { ...running, id: "s", label: "Clone", state: "succeeded" });
assert.ok(!window.reloaded && elo.innerHTML.includes("no route") && elo.innerHTML.includes("Clone"),
"the failure stays, the success shows beside it");
answer("/j/w", { ...running, id: "w", label: "Snapshot", state: "succeeded" });
assert.ok(!window.reloaded, "still not while an outcome is shown");
open = []; timers = []; window.reloaded = false;
const elp = { innerHTML: "", addEventListener() {} };
jobPanel(elp, urls, "tok", ["s2", "f2"], STATES, TEXT);
advance(0);
answer("/r", empty);
answer("/j/s2", { ...running, id: "s2", state: "succeeded" });
assert.ok(!window.reloaded, "not while another outcome is awaited");
answer("/j/f2", { ...running, id: "f2", state: "failed", error: "refused" });
assert.ok(elp.innerHTML.includes("refused"), "which then shows");
// another job still running: its outcome would be lost to a reload
open = []; timers = []; window.reloaded = false;
const elr = { innerHTML: "", addEventListener() {} };
jobPanel(elr, urls, "tok", ["s3"], STATES, TEXT);
advance(0);
answer("/r", { ...empty, jobs: [{ ...running, id: "r3" }] });
answer("/j/s3", { ...running, id: "s3", state: "succeeded" });
assert.ok(!window.reloaded, "not while another job runs");
Date.now = realNow;
console.log("ok");
// the page's language: the badge, the buttons and the panel's texts come from its settings
{
const elt = { innerHTML: "", addEventListener() {} };
const states = { ...STATES, running: "çalışıyor" };
jobPanel(elt, urls, "tok", undefined, states, { ...TEXT, cancel: "İptal" });
advance(0);
answer("/r", { ...empty, jobs: [{ ...running, id: "t", cancellable: true }] });
assert.ok(elt.innerHTML.includes('çalışıyor')
&& elt.innerHTML.includes(">İptal"), "labels and texts of the page's language");
}