mirror of
https://github.com/logos-storage/logos-storage-modules-e2e.git
synced 2026-06-13 19:09:27 +00:00
170 lines
6.5 KiB
JavaScript
170 lines
6.5 KiB
JavaScript
|
|
#!/usr/bin/env node
|
||
|
|
import { resolve } from "node:path";
|
||
|
|
import { strict as assert } from "node:assert";
|
||
|
|
|
||
|
|
const MAGIC_RETRY_NUMBER = 3;
|
||
|
|
|
||
|
|
const qtMcpRoot = process.env.LOGOS_QT_MCP
|
||
|
|
const {run, test} = await import(resolve(qtMcpRoot, "test-framework/framework.mjs"));
|
||
|
|
|
||
|
|
const storageCoreLgx = process.env.STORAGE_CORE_LGX;
|
||
|
|
const storageUiLgx = process.env.STORAGE_UI_LGX;
|
||
|
|
|
||
|
|
async function sleepAsync(ms) {
|
||
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||
|
|
}
|
||
|
|
|
||
|
|
async function sha1digest(path) {
|
||
|
|
const { createHash } = await import("node:crypto");
|
||
|
|
const { readFile } = await import("node:fs/promises");
|
||
|
|
const hash = createHash("sha1");
|
||
|
|
hash.update(await readFile(path));
|
||
|
|
return hash.digest("hex");
|
||
|
|
}
|
||
|
|
|
||
|
|
async function genRandomFile(path, size) {
|
||
|
|
const { randomBytes } = await import("node:crypto");
|
||
|
|
const { writeFile } = await import("node:fs/promises");
|
||
|
|
await writeFile(path, randomBytes(size));
|
||
|
|
}
|
||
|
|
|
||
|
|
async function findDialogByProperty(app, propertyName, value) {
|
||
|
|
const response = await app.listFileDialogs();
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error(response.error);
|
||
|
|
}
|
||
|
|
const dialog = response.dialogs.find(dialog => dialog[propertyName] === value);
|
||
|
|
if (!dialog) {
|
||
|
|
throw new Error(`Dialog with property ${propertyName} = ${value} not found`);
|
||
|
|
}
|
||
|
|
return dialog;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function findDialogByTitle(app, title) {
|
||
|
|
return findDialogByProperty(app, "title", title);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Accepts a file dialog, while handling accept flakiness both for
|
||
|
|
// QFileDialog and QQuickFileDialog.
|
||
|
|
async function accept(app, dialogId) {
|
||
|
|
try {
|
||
|
|
// Accepts will sometimes be ignored, so we need to try multiple times.
|
||
|
|
do {
|
||
|
|
await app.fileDialogAction(dialogId, "accept");
|
||
|
|
await sleepAsync(300);
|
||
|
|
await findDialogByProperty(app, "id", dialogId);
|
||
|
|
// QQuickDialogs remain in the object tree but their "visible" state
|
||
|
|
// changes, so that's what we probe for.
|
||
|
|
let visible = await app.getProperty(dialogId, "visible");
|
||
|
|
if (!visible) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
} while (true);
|
||
|
|
} catch (error) {
|
||
|
|
// For QFileDialog, the dialog will simply disappear once accept succeeds,
|
||
|
|
// which will cause subsequent find and click attempts to raise errors.
|
||
|
|
// So getting here means we're done.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Reliably selects a folder in QQuickFileDialog while dealing with all
|
||
|
|
// the flakiness. Not needed for QDialog.
|
||
|
|
async function qtquickFileDialogSelect(app, dialogId, triggerName, path, folder) {
|
||
|
|
for (let i = 0; i < MAGIC_RETRY_NUMBER; i++) {
|
||
|
|
// With QQuickFileDialog, we need to select BEFORE clicking the trigger button.
|
||
|
|
await app.fileDialogAction(dialogId, "select", path);
|
||
|
|
await app.clickByProperty("objectName", triggerName);
|
||
|
|
await app.waitFor(() => app.expectPropertyOnObject(dialogId, "visible", true));
|
||
|
|
// Folders, on the other hand, need to be set AFTER the dialog is visible or it won't work!
|
||
|
|
if (folder) {
|
||
|
|
await app.fileDialogAction(dialogId, "selectFolder", folder);
|
||
|
|
}
|
||
|
|
// Even then, it might happen that selection does not work. In that case, we try again.
|
||
|
|
try {
|
||
|
|
await app.waitFor(() => app.expectPropertyOnObject(dialogId, "selectedFile", `file://${path}`));
|
||
|
|
return;
|
||
|
|
} catch (error) {
|
||
|
|
// Cancel and try again.
|
||
|
|
await app.fileDialogAction(dialogId, "cancel");
|
||
|
|
await app.waitFor(() => app.expectPropertyOnObject(dialogId, "visible", false));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
throw new Error("Failed to select file in dialog");
|
||
|
|
}
|
||
|
|
|
||
|
|
async function selectCard(app, cardName) {
|
||
|
|
await app.waitFor(async() => {
|
||
|
|
await app.clickByProperty("objectName", `${cardName}MouseArea`);
|
||
|
|
const card = await app.findByProperty("objectName", cardName);
|
||
|
|
await app.expectPropertyOnObject(card.matches[0].id, "selected", true);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
async function uiInstallModule(app, moduleTab, modulePath) {
|
||
|
|
console.log("Install module ", modulePath, " in tab ", moduleTab);
|
||
|
|
await app.click("Modules");
|
||
|
|
await app.click(moduleTab);
|
||
|
|
await app.click("Install LGX Package");
|
||
|
|
let dialog = await app.waitFor(
|
||
|
|
() => findDialogByTitle(app, "Select Plugin to Install")
|
||
|
|
);
|
||
|
|
await app.fileDialogAction(dialog.id, "select", modulePath);
|
||
|
|
await accept(app, dialog.id);
|
||
|
|
await app.expectTexts(["Install"]);
|
||
|
|
await app.click("Install", {exact: true});
|
||
|
|
console.log("Install succeeded.");
|
||
|
|
}
|
||
|
|
|
||
|
|
test("storage: assert basic module functionality", async (app) => {
|
||
|
|
// Installs modules.
|
||
|
|
await uiInstallModule(app, "Core Modules", storageCoreLgx);
|
||
|
|
await uiInstallModule(app, "UI Modules", storageUiLgx);
|
||
|
|
|
||
|
|
// Opens the storage app.
|
||
|
|
console.log("1. Opening storage.");
|
||
|
|
await app.waitFor(() => app.click("Storage"));
|
||
|
|
|
||
|
|
// Goes through the onboarding guide. Note that these options
|
||
|
|
// may not work on your machine.
|
||
|
|
console.log("2. Onboarding guide: step 1.");
|
||
|
|
await app.waitFor(() => app.expectTexts(["Guided"]));
|
||
|
|
await selectCard(app, "guidedCard");
|
||
|
|
await app.click("Continue");
|
||
|
|
|
||
|
|
console.log("3. Onboarding guide: step 2.");
|
||
|
|
await app.waitFor(() => app.expectTexts(["UPnP"]));
|
||
|
|
await selectCard(app, "upnpCard"); // In CI should use Port Forwarding
|
||
|
|
await app.waitFor(() => app.click("Continue"));
|
||
|
|
|
||
|
|
console.log("4. Onboarding guide: wait for node to be connected.");
|
||
|
|
await app.waitFor(() => app.expectTexts(
|
||
|
|
["Your node is up and reachable."],
|
||
|
|
{timeout: 20000, interval: 500, description: "Waiting for node to be up and reachable"}
|
||
|
|
));
|
||
|
|
await app.click("Continue");
|
||
|
|
|
||
|
|
console.log("5. Publish file.");
|
||
|
|
// Sets up a random file and publishes it.
|
||
|
|
await genRandomFile("/tmp/testfile.bin", 5 * 1024 * 1024); // 5MB file
|
||
|
|
const uploadDialog = await app.waitFor(() => findDialogByProperty(app, "objectName", "uploadDialog"));
|
||
|
|
await app.waitFor(() => qtquickFileDialogSelect(app, uploadDialog.id, "uploadButton", "/tmp/testfile.bin"));
|
||
|
|
await accept(app, uploadDialog.id);
|
||
|
|
await app.waitFor(() => app.expectTexts(["Complete"]));
|
||
|
|
|
||
|
|
console.log("6. Download file.");
|
||
|
|
// Downloads the file we just uploaded to a different location.
|
||
|
|
const saveDialog = await app.waitFor(() => findDialogByProperty(app, "objectName", "saveDialog"));
|
||
|
|
// We can't change the filename in the dialog, so write to the current folder.
|
||
|
|
await qtquickFileDialogSelect(app, saveDialog.id, "downloadButton", `${process.env.PWD}/testfile.bin`, process.env.PWD);
|
||
|
|
await accept(app, saveDialog.id);
|
||
|
|
|
||
|
|
console.log("7. Check file integrity.");
|
||
|
|
// Checks that the file was downloaded correctly.
|
||
|
|
const downloadedFile = `${process.env.PWD}/testfile.bin`;
|
||
|
|
const originalDigest = await sha1digest("/tmp/testfile.bin");
|
||
|
|
const downloadedDigest = await sha1digest(downloadedFile);
|
||
|
|
assert(originalDigest === downloadedDigest, "File digest mismatch");
|
||
|
|
});
|
||
|
|
|
||
|
|
run();
|