commit 8d8866d7241788861ec3d7e614167179d18ed97c Author: gmega Date: Tue Jun 9 17:28:47 2026 -0300 add e2e UI test diff --git a/tests/logos-ui-module-tests.mjs b/tests/logos-ui-module-tests.mjs new file mode 100644 index 0000000..534224f --- /dev/null +++ b/tests/logos-ui-module-tests.mjs @@ -0,0 +1,169 @@ +#!/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();