Thing/packages/eve-browser-lowering/test/host-isolation.test.mjs

420 lines
15 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import { JSDOM } from "jsdom";
import { EveBrowserDraftStore, renderEveSurface } from "../dist/index.js";
function installDom() {
const dom = new JSDOM("<!doctype html><html><head></head><body><div id='a'></div><div id='b'></div></body></html>", {
url: "https://eve.test/",
pretendToBeVisual: true,
});
const names = [
"document",
"window",
"Element",
"HTMLElement",
"HTMLButtonElement",
"HTMLInputElement",
"HTMLSelectElement",
"HTMLOptionElement",
"HTMLTextAreaElement",
"HTMLImageElement",
];
const previous = new Map(names.map(name => [name, globalThis[name]]));
for (const name of names) globalThis[name] = dom.window[name];
return () => {
for (const [name, value] of previous) {
if (value === undefined) delete globalThis[name];
else globalThis[name] = value;
}
dom.window.close();
};
}
function testInputAdapter(onRender = () => {}) {
return {
pluginId: "test.input",
componentKinds: ["test.input"],
renderComponent(_node, props) {
onRender();
const input = document.createElement("input");
input.type = "text";
input.value = String(props.value ?? "");
return input;
},
};
}
function surface(providerId, imagePath, skinPart, inputComponent) {
return {
providerId,
surface: {
id: `${providerId}.hangar`,
styles: {
tokens: { colorAccent: providerId === "alpha" ? "#a00" : "#00a" },
controlSkins: {
custom: {
children: [{ kind: "control.part", props: { name: skinPart } }],
},
},
},
root: {
id: `${providerId}.root`,
kind: "column",
children: [
{ kind: "image.preview", props: { src: imagePath, label: providerId } },
{ kind: "control.slider", props: { skin: "custom", value: 0.5 } },
inputComponent,
{ kind: "control.button", props: { label: `Launch ${providerId}`, command: `${providerId}.launch` } },
],
},
},
};
}
test("browser lowering isolates two hosts and patches only the bound component", async () => {
const restoreDom = installDom();
try {
let applyAlphaBinding;
let alphaInputRenders = 0;
const alphaCommands = [];
const betaCommands = [];
const alphaInput = {
id: "alpha.input",
kind: "test.input",
props: { value: "initial" },
stateBindings: [{
targetProp: "value",
pointerId: "alpha.input.value",
sourceId: "alpha.state",
schemaId: "alpha.state.v1",
routeKind: "cultmesh",
}],
};
const alphaSurface = surface("alpha", "/ship.png", "track-alpha", alphaInput);
const betaSurface = surface("beta", "/ship.png", "track-beta", {
id: "beta.input",
kind: "test.input",
props: { value: "beta" },
});
const alphaHost = document.querySelector("#a");
const betaHost = document.querySelector("#b");
renderEveSurface(alphaSurface, alphaHost, {
assetUrlResolver: uri => `https://alpha.assets${uri}`,
commandSink: intent => alphaCommands.push(intent),
pluginAdapters: [testInputAdapter(() => { alphaInputRenders++; })],
provider: { providerId: "alpha", surfaces: [{ surfaceId: "alpha.hangar", worldInteraction: { commandBoundary: "alpha.commands", receiptSchema: "gamecult.eve.command_receipt.v1" } }] },
stateBindingResolver: async () => ({
latest: async () => "hydrated",
watch: callback => {
applyAlphaBinding = callback;
return () => { applyAlphaBinding = undefined; };
},
}),
});
renderEveSurface(betaSurface, betaHost, {
assetUrlResolver: uri => `https://beta.assets${uri}`,
commandSink: intent => betaCommands.push(intent),
pluginAdapters: [testInputAdapter()],
provider: { providerId: "beta", surfaces: [{ surfaceId: "beta.hangar", worldInteraction: { commandBoundary: "beta.commands", receiptSchema: "gamecult.eve.command_receipt.v1" } }] },
});
await new Promise(resolve => setTimeout(resolve, 0));
assert.equal(alphaHost.querySelector("img").src, "https://alpha.assets/ship.png");
assert.equal(betaHost.querySelector("img").src, "https://beta.assets/ship.png");
assert.ok(alphaHost.querySelector(".track-alpha"));
assert.ok(betaHost.querySelector(".track-beta"));
alphaHost.querySelector("button").click();
betaHost.querySelector("button").click();
assert.equal(alphaCommands[0].providerId, "alpha");
assert.equal(alphaCommands[0].surfaceId, "alpha.hangar");
assert.equal(betaCommands[0].providerId, "beta");
assert.equal(betaCommands[0].surfaceId, "beta.hangar");
const alphaRoot = alphaHost.firstElementChild;
const betaRoot = betaHost.firstElementChild;
const focusedInput = alphaHost.querySelector("input[type='text']");
focusedInput.focus();
focusedInput.setSelectionRange(1, 4);
alphaHost.scrollTop = 37;
const rendersBeforeUpdates = alphaInputRenders;
for (let index = 0; index < 1000; index++) applyAlphaBinding(`updated-${index}`);
await new Promise(resolve => setTimeout(resolve, 0));
const updatedInput = alphaHost.querySelector("input[type='text']");
assert.equal(alphaHost.firstElementChild, alphaRoot);
assert.equal(betaHost.firstElementChild, betaRoot);
assert.equal(updatedInput.value, "updated-999");
assert.equal(alphaInputRenders, rendersBeforeUpdates + 1);
assert.equal(document.activeElement, updatedInput);
assert.equal(updatedInput.selectionStart, 1);
assert.equal(updatedInput.selectionEnd, 4);
assert.equal(alphaHost.scrollTop, 37);
} finally {
restoreDom();
}
});
test("browser lowering renders command-backed select options", () => {
const restoreDom = installDom();
try {
const commands = [];
const host = document.querySelector("#a");
renderEveSurface({
providerId: "hangar-provider",
surface: {
id: "hangar",
root: {
id: "hangar.verse",
kind: "control.select",
props: { label: "VERSE", value: "local", command: "hangar.select_verse" },
children: [
{ kind: "control.option", props: { label: "Local", value: "local" } },
{ kind: "control.option", props: { label: "GameCult", value: "gamecult" } },
],
},
},
}, host, {
clientId: "browser-test",
commandSink: intent => commands.push(intent),
provider: { providerId: "hangar-provider", surfaces: [{ surfaceId: "hangar", worldInteraction: { commandBoundary: "hangar.commands", receiptSchema: "gamecult.eve.command_receipt.v1" } }] },
});
const select = host.querySelector("select");
assert.deepEqual([...select.options].map(option => [option.text, option.value]), [
["Local", "local"],
["GameCult", "gamecult"],
]);
assert.equal(select.value, "local");
select.value = "gamecult";
select.dispatchEvent(new window.Event("change", { bubbles: true }));
assert.equal(commands.length, 1);
assert.equal(commands[0].operation.operationId, "hangar.select_verse");
assert.equal(commands[0].payload.value, "gamecult");
} finally {
restoreDom();
}
});
test("editable bindings preserve local drafts across authoritative refresh and operations capture them", () => {
const restoreDom = installDom();
try {
const commands = [];
const drafts = new EveBrowserDraftStore();
const host = document.querySelector("#a");
const provider = {
providerId: "ghostlight",
surfaces: [{ surfaceId: "ghostlight.play", worldInteraction: {
commandBoundary: "ghostlight.eve.commands",
receiptSchema: "gamecult.eve.command_result.v1",
} }],
};
const surface = value => ({
providerId: "ghostlight",
version: 7,
commands: [{
command: "session_zero.message.send",
payloadSchema: "session_zero.message.send.v1",
captureBindings: ["composer.message"],
}],
surface: {
id: "ghostlight.play",
root: { id: "root", kind: "column", children: [
{
id: "composer",
kind: "control.input.textarea",
props: { label: "Message", value },
stateBindings: [{
targetProp: "value",
pointerId: "ghostlight.local.composer.message",
sourceId: "eve.browser.local",
schemaId: "gamecult.eve.local_draft.v1",
routeKind: "in-process",
bindingName: "composer.message",
valueKind: "string",
accessMode: "local-draft",
authority: "eve.browser",
}],
},
{ id: "send", kind: "control.button", props: { label: "Send", command: "session_zero.message.send" } },
] },
},
});
const options = { provider, draftStore: drafts, commandSink: intent => commands.push(intent) };
renderEveSurface(surface(""), host, options);
const composer = host.querySelector("textarea");
composer.value = "The world should remember this.";
composer.dispatchEvent(new window.Event("input", { bubbles: true }));
renderEveSurface(surface("stale provider value"), host, options);
assert.equal(host.querySelector("textarea").value, "The world should remember this.");
host.querySelector("button").click();
assert.deepEqual(commands[0].payload.bindings, {
"composer.message": "The world should remember this.",
});
assert.equal(commands[0].operation.schemaId, "session_zero.message.send.v1");
assert.equal(commands[0].operation.routeHint.sourceVersion, 7);
drafts.clear("ghostlight", "ghostlight.play", ["composer.message"]);
renderEveSurface(surface("accepted provider value"), host, options);
assert.equal(host.querySelector("textarea").value, "accepted provider value");
} finally {
restoreDom();
}
});
test("operations capture untouched authored choice defaults", () => {
const restoreDom = installDom();
try {
const commands = [];
const host = document.querySelector("#a");
const provider = {
providerId: "ghostlight",
surfaces: [{ surfaceId: "ghostlight.play", worldInteraction: {
commandBoundary: "ghostlight.eve.commands",
receiptSchema: "gamecult.eve.command_result.v1",
} }],
};
renderEveSurface({
providerId: "ghostlight",
version: 7,
commands: [{
command: "session_zero.message.send",
payloadSchema: "session_zero.message.send.v1",
captureBindings: ["channel_id"],
}],
surface: {
id: "ghostlight.play",
root: { id: "root", kind: "column", children: [
{
id: "channel",
kind: "control.select",
props: { label: "Speak at", value: "shared" },
stateBindings: [{
targetProp: "value",
pointerId: "ghostlight.local.channel_id",
sourceId: "eve.browser.local",
schemaId: "gamecult.eve.local_draft.v1",
routeKind: "in-process",
bindingName: "channel_id",
valueKind: "choice",
accessMode: "local-draft",
authority: "eve.browser",
}],
children: [
{ kind: "control.option", props: { label: "Shared", value: "shared" } },
{ kind: "control.option", props: { label: "Private", value: "private" } },
],
},
{ id: "send", kind: "control.button", props: { label: "Send", command: "session_zero.message.send" } },
] },
},
}, host, {
provider,
draftStore: new EveBrowserDraftStore(),
commandSink: intent => commands.push(intent),
});
assert.equal(host.querySelector("select").value, "shared");
host.querySelector("button").click();
assert.deepEqual(commands[0].payload.bindings, { channel_id: "shared" });
} finally {
restoreDom();
}
});
test("component captures extend rather than disappear behind operation descriptors", () => {
const restoreDom = installDom();
try {
const commands = [];
const host = document.querySelector("#a");
const provider = {
providerId: "ghostlight",
surfaces: [{ surfaceId: "ghostlight.play", worldInteraction: {
commandBoundary: "ghostlight.eve.commands",
receiptSchema: "gamecult.eve.command_result.v1",
} }],
};
renderEveSurface({
providerId: "ghostlight",
version: 17,
commands: [{
command: "session_zero.decision.resolve",
payloadSchema: "session_zero.decision.resolve.v1",
captureBindings: [],
}],
surface: {
id: "ghostlight.play",
root: { id: "root", kind: "column", children: [
{
id: "counter",
kind: "control.input.textarea",
props: { label: "Counterproposal" },
stateBindings: [{
targetProp: "value",
pointerId: "draft:counter",
sourceId: "renderer",
schemaId: "gamecult.eve.local_draft.v1",
routeKind: "local",
bindingName: "counter",
valueKind: "string",
accessMode: "local-draft",
authority: "renderer-ephemeral",
}],
},
{
id: "counter-decision",
kind: "control.button",
props: {
label: "Counter",
command: "session_zero.decision.resolve",
action: { decision_id: "decision:one", accept: false },
captureBindings: ["counter"],
},
},
] },
},
}, host, {
provider,
draftStore: new EveBrowserDraftStore(),
commandSink: intent => commands.push(intent),
});
const counter = host.querySelector("textarea");
counter.value = "Contamination fades, but scars remain.";
counter.dispatchEvent(new window.Event("input", { bubbles: true }));
host.querySelector("button").click();
assert.deepEqual(commands[0].payload.bindings, {
counter: "Contamination fades, but scars remain.",
});
} finally {
restoreDom();
}
});
test("authoritative refresh preserves the renderer-owned command result", () => {
const restoreDom = installDom();
try {
const host = document.querySelector("#a");
const surface = version => ({
providerId: "ghostlight",
version,
surface: { id: "ghostlight.play", root: { id: `root-${version}`, kind: "text", props: { value: `revision ${version}` } } },
});
renderEveSurface(surface(7), host);
const result = document.createElement("section");
result.className = "eve-command-result-region";
result.setAttribute("role", "error");
result.textContent = "The command was denied.";
host.append(result);
renderEveSurface(surface(8), host);
assert.equal(host.querySelector(".eve-command-result-region"), result);
assert.equal(result.textContent, "The command was denied.");
assert.match(host.textContent, /revision 8/);
} finally {
restoreDom();
}
});