#!/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();