Files
lez-programs/apps/token/tests/token-definition.mjs
Ricardo Guilherme Schmidt 0c04df87cc feat(token-ui): integrate Basecamp token module
Add portable Basecamp packaging and an inspector-driven end-to-end flow.
2026-08-20 13:30:33 +02:00

250 lines
9.1 KiB
JavaScript

// Token UI end-to-end test for Logos Basecamp.
//
// Normal mode drives the complete visible create -> inspect shell without
// mutating chain state. Set TOKEN_E2E_LIVE=1 to create fresh wallet accounts,
// submit a fixed-supply definition through token_module, and verify it appears
// in the live Inspect view.
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { mkdir, writeFile } from "node:fs/promises";
const fwRoot =
process.env.LOGOS_QT_MCP ||
new URL("../result-mcp", import.meta.url).pathname;
const { test, run } = await import(resolve(fwRoot, "test-framework/framework.mjs"));
const live = process.env.TOKEN_E2E_LIVE === "1";
const evidenceDir = process.env.TOKEN_E2E_OUTPUT
? resolve(process.env.TOKEN_E2E_OUTPUT)
: fileURLToPath(
new URL(
"../../../.3esmit/projects/lez-programs/docs/token-basecamp-e2e/",
import.meta.url,
),
);
const tokenName = process.env.TOKEN_E2E_NAME || `Basecamp E2E ${Date.now()}`;
async function idByObjectName(app, name) {
const result = await app.findByProperty("objectName", name);
if (result.error || !result.matches || result.matches.length === 0)
throw new Error(`no object with objectName="${name}"`);
return result.matches[0].id;
}
async function prop(app, objectId, property) {
const result = await app.getProperties(objectId);
const properties = result.properties || [];
const found = properties.find((item) => item.name === property);
return found ? found.value : undefined;
}
async function setProp(app, objectId, property, value) {
const result = await app.inspector.send("setProperty", {
objectId,
property,
value,
});
if (result.error)
throw new Error(`setProperty ${property}: ${result.error}`);
}
async function clickObject(app, name) {
const objectId = await idByObjectName(app, name);
const result = await app.inspector.send("click", { objectId });
if (result.error)
throw new Error(`click ${name}: ${result.error}`);
return objectId;
}
async function clickFirstVisible(app, names) {
for (const name of names) {
const result = await app.findByProperty("objectName", name);
for (const match of result.matches || []) {
if ((await prop(app, match.id, "visible")) !== true)
continue;
if ((await prop(app, match.id, "enabled")) === false)
continue;
const clickResult = await app.inspector.send("click", { objectId: match.id });
if (clickResult.error)
throw new Error(`click ${name}: ${clickResult.error}`);
return match.id;
}
}
throw new Error(`no visible enabled object: ${names.join(", ")}`);
}
async function waitForProperty(app, objectId, property, predicate, description, timeout = 10000) {
await app.waitFor(
async () => {
const value = await prop(app, objectId, property);
if (!predicate(value))
throw new Error(`${property}=${JSON.stringify(value)}`);
},
{ timeout, interval: 300, description },
);
}
async function saveShot(app, name) {
const shot = await app.screenshot();
if (!shot || !shot.image)
throw new Error(`screenshot unavailable for ${name}`);
await mkdir(evidenceDir, { recursive: true });
const path = resolve(evidenceDir, `${name}.png`);
await writeFile(path, Buffer.from(shot.image, "base64"));
console.log(` screenshot -> ${path}`);
}
async function chooseFixedTemplate(app) {
await clickObject(app, "tokenExamplesButton");
await app.waitFor(
async () => {
const menuId = await idByObjectName(app, "tokenExamplesMenu");
if ((await prop(app, menuId, "visible")) !== true)
throw new Error("template menu is closed");
},
{ timeout: 3000, interval: 150, description: "template menu to open" },
);
await clickObject(app, "tokenFixedTemplate");
const nameFieldId = await idByObjectName(app, "tokenNameField");
await waitForProperty(
app,
nameFieldId,
"text",
(value) => value === "Fixed supply token",
"fixed template values",
);
}
async function openTokenApp(app) {
await app.waitFor(
async () => { await app.expectTexts(["Applications", "Settings"]); },
{ timeout: 60000, interval: 500, description: "Basecamp shell to load" },
);
await saveShot(app, "basecamp-launch");
const labels = ["Token", "token_ui", "Logos Token"];
await app.waitFor(
async () => {
for (const label of labels) {
const result = await app.findByProperty("text", label);
if (result.matches && result.matches.length > 0) {
await app.click(label);
return;
}
}
throw new Error(`Token app not visible in Basecamp sidebar (${labels.join(", ")})`);
},
{ timeout: 30000, interval: 500, description: "Token app in Basecamp sidebar" },
);
}
test(live
? "token definition: create through token_module and inspect live state"
: "token UI: create shell flows into Inspect", async (app) => {
await openTokenApp(app);
await app.waitFor(
async () => { await app.expectTexts(["Create definition", "Fungible", "Use template"]); },
{ timeout: 20000, interval: 400, description: "token create view to load" },
);
const createPageId = await idByObjectName(app, "tokenCreatePage");
await saveShot(app, "token-create-initial");
await chooseFixedTemplate(app);
const nameFieldId = await idByObjectName(app, "tokenNameField");
await setProp(app, nameFieldId, "text", tokenName);
await saveShot(app, "token-create-template");
await clickFirstVisible(app, ["tokenContinueButton", "tokenSummaryContinueButton"]);
await waitForProperty(
app,
createPageId,
"step",
(value) => value === 1,
"account-target step",
);
if (!live) {
const createAccountsButtonId = await idByObjectName(app, "tokenCreateAccountsButton");
if (await prop(app, createAccountsButtonId, "enabled") === true)
throw new Error("visual mode unexpectedly has a live wallet");
await saveShot(app, "token-account-targets-disconnected");
await app.click("Inspect");
await app.waitFor(
async () => { await app.expectTexts(["Inspect token definitions", "Definition index"]); },
{ timeout: 10000, interval: 300, description: "Inspect view to load" },
);
await app.waitFor(
async () => {
const result = await app.findByProperty("text", "Pebble");
if (!result.matches || result.matches.length === 0)
throw new Error("example definitions not rendered");
},
{ timeout: 5000, interval: 250, description: "example definitions" },
);
await saveShot(app, "token-inspect-examples");
console.log(" mode: visual shell; chain state unchanged");
return;
}
const createAccountsButtonId = await idByObjectName(app, "tokenCreateAccountsButton");
await waitForProperty(
app,
createAccountsButtonId,
"enabled",
(value) => value === true,
"connected wallet for fresh accounts",
5000,
);
await clickObject(app, "tokenCreateAccountsButton");
const definitionTargetId = await idByObjectName(app, "tokenDefinitionTargetField");
const holdingTargetId = await idByObjectName(app, "tokenHoldingTargetField");
const validAccount = (value) => typeof value === "string" && value.length > 0;
await waitForProperty(app, definitionTargetId, "text", validAccount, "definition target account", 20000);
await waitForProperty(app, holdingTargetId, "text", validAccount, "holding target account", 20000);
await saveShot(app, "token-account-targets-ready");
await clickObject(app, "tokenReviewButton");
await waitForProperty(app, createPageId, "step", (value) => value === 2, "definition review");
await saveShot(app, "token-definition-review");
const prepareButtonId = await idByObjectName(app, "tokenPrepareButton");
await waitForProperty(app, prepareButtonId, "enabled", (value) => value === true, "create definition action");
await clickObject(app, "tokenPrepareButton");
await app.waitFor(
async () => {
const prepared = await prop(app, createPageId, "prepared");
const error = await prop(app, createPageId, "errorMessage");
if (prepared !== true && !error)
throw new Error("transaction is still pending");
if (error)
throw new Error(`Token Program rejected request: ${error}`);
},
{ timeout: 60000, interval: 500, description: "Token Program submission" },
);
await saveShot(app, "token-definition-submitted");
await app.click("Inspect");
await app.waitFor(
async () => { await app.expectTexts(["Inspect token definitions", "Definition index"]); },
{ timeout: 10000, interval: 300, description: "live Inspect view" },
);
await app.waitFor(
async () => {
try { await clickObject(app, "tokenRefreshButton"); } catch { /* refresh may be busy */ }
const result = await app.findByProperty("text", tokenName);
if (!result.matches || result.matches.length === 0)
throw new Error(`live definition ${tokenName} not indexed yet`);
},
{ timeout: 60000, interval: 1200, description: "created definition in live Inspect view" },
);
await app.expectTexts([tokenName, "Network"]);
await saveShot(app, "token-inspect-live-definition");
console.log(` mode: live; verified ${tokenName} in wallet-backed Inspect view`);
});
run();