mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 14:11:09 +00:00
Add portable Basecamp packaging and an inspector-driven end-to-end flow.
250 lines
9.1 KiB
JavaScript
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();
|