"use strict"; // End-to-end UI check of chj.disks with the real plugin runtime, preload, DiskService and an isolated test VM. // Linux VM (default): CHJ_VM_HOST/_FINGERPRINT/_KEY and CHJ_VM_SUDO_PASSWORD or _FILE + _LINE. Reads the inventory // through the sudo dialog, then creates a GPT + ext4 partition on a dedicated loop image (/var/tmp/chj-disks-test) // through the context menu, queue, review dialog and job monitor. The system disk is never changed. // Windows: CHJ_SMOKE_WINDOWS=1 with CHJ_DISKS_WINDOWS_HOST/_FINGERPRINT/_KEY: the "unsupported platform" view. // Run (VS Code terminal): env -u ELECTRON_RUN_AS_NODE npx electron scripts/smokeDisksUi.js [screenshot-dir] const { app, BrowserWindow, ipcMain, protocol } = require("electron"); const assert = require("node:assert/strict"); const crypto = require("node:crypto"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const { hardenSession } = require("../src/main/bootstrap/createMainWindow"); const { PluginRegistry } = require("../src/main/plugins/pluginRegistry"); const { PluginRuntime } = require("../src/main/plugins/pluginRuntime"); const { SessionManager } = require("../src/main/sessions/sessionManager"); const { PrivilegedExecutor } = require("../src/main/sessions/privilegedExec"); const { DiskService } = require("../src/main/disks/diskService"); const env = process.env; const windows = env.CHJ_SMOKE_WINDOWS === "1"; const sudoPassword = windows ? "" : env.CHJ_VM_SUDO_PASSWORD || fs.readFileSync(env.CHJ_VM_SUDO_PASSWORD_FILE, "utf8").split("\n")[Number(env.CHJ_VM_SUDO_PASSWORD_LINE || 1) - 1].split("=").slice(1).join("=").trim(); const FIXTURE = fs.readFileSync(path.join(__dirname, "../test/fixtures/disks/loopFixture.sh"), "utf8"); const RUN = crypto.randomBytes(4).toString("hex"); const root = fs.mkdtempSync(path.join(os.tmpdir(), "chj-disks-ui-")); const shots = process.argv.find((arg, index) => index > 1 && !arg.startsWith("-") && !arg.endsWith(".js")) || null; app.setPath("userData", root); protocol.registerSchemesAsPrivileged([{ scheme: "chj-plugin", privileges: { standard: true, secure: true, supportFetchAPI: true } }]); const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const errors = []; let window; let executor; let fixtureUsed = false; const js = (source) => window.webContents.executeJavaScript(source); async function until(source, timeout = 90000) { const started = Date.now(); while (Date.now() - started < timeout) { if (await js(`(() => { try { return (${source}); } catch { return false; } })()`)) return; await delay(150); } throw new Error(`UI condition timed out: ${source}\nstatus: ${await js("document.querySelector('#status').textContent + ' | ' + (document.querySelector('#formError')?.textContent || '') + ' | ' + (document.querySelector('#applyError')?.textContent || '')")}`); } async function shot(name) { if (shots) { fs.mkdirSync(shots, { recursive: true }); fs.writeFileSync(path.join(shots, `${name}.png`), (await window.webContents.capturePage()).toPNG()); } } // Answers the sudo dialog whenever Core asks for the password while waiting for doneSource. async function untilWithSudo(doneSource, timeout = 120000) { for (const started = Date.now(); Date.now() - started < timeout; await delay(150)) { if (await js("document.querySelector('#sudoDialog').open")) await js(`document.querySelector('#sudoPassword').value = ${JSON.stringify(sudoPassword)}; document.querySelector('#sudoForm').requestSubmit();`); if (await js(`(() => { try { return (${doneSource}); } catch { return false; } })()`)) return; } throw new Error(`UI condition timed out: ${doneSource}\nstatus: ${await js("document.querySelector('#status').textContent")}`); } const fixture = async (...args) => { fixtureUsed = true; const run = await executor.run("ui", { script: FIXTURE, args, privileged: true, sudoPassword, purpose: "test.disks.fixture", timeoutMs: 120000, errorPrefix: "FIXTURE_" }); return Object.fromEntries(run.stdout.split("\n").filter((line) => line.includes("=")).map((line) => [line.slice(0, line.indexOf("=")), line.slice(line.indexOf("=") + 1)])); }; async function run() { const host = windows ? env.CHJ_DISKS_WINDOWS_HOST : env.CHJ_VM_HOST; const fingerprint = windows ? env.CHJ_DISKS_WINDOWS_FINGERPRINT : env.CHJ_VM_FINGERPRINT; const profile = { id: "p", label: windows ? "Windows test VM" : "Linux test VM", host, port: 22, username: (windows ? env.CHJ_DISKS_WINDOWS_USER : env.CHJ_VM_USER) || "ai", authMethod: "privateKey", privateKeyPath: windows ? env.CHJ_DISKS_WINDOWS_KEY : env.CHJ_VM_KEY }; await app.whenReady(); hardenSession(); const hostKeys = new Map([[22, Buffer.from(`${fingerprint.replace(/^SHA256:/, "")}=`, "base64").toString("hex")]]); const profileService = { get: () => profile, getHostKey: (_h, p) => hostKeys.get(p) || null, trustHostKey: (_h, p, fp) => hostKeys.set(p, fp), getStoredPassword: () => "", markUsed() {} }; const sessionManager = new SessionManager({ profileService, latencyMonitor: { start: () => () => {} } }); await sessionManager.connect({ sessionId: "ui", profileId: "p" }); executor = new PrivilegedExecutor({ sessionManager }); const diskService = new DiskService({ sessionManager, privilegedExecutor: executor }); const registry = new PluginRegistry(root, null, { bundledRoot: path.join(__dirname, "../src/main/firstPartyPlugins") }); const language = env.CHJ_SMOKE_LANGUAGE || "cs"; const runtime = new PluginRuntime({ BrowserWindow, registry, sessionManager, diskService, getLanguage: () => language, preload: path.join(__dirname, "../src/preload/pluginPreload.js"), getMainWindow: () => null, isVaultUnlocked: () => true }); runtime.registerIpc(ipcMain); runtime.open("chj.disks"); window = runtime.windows.get("chj.disks"); window.webContents.on("console-message", (event) => { if (event.level === "error") errors.push(event.message); }); window.setSize(1360, 1000); try { if (windows) { await until("!document.querySelector('#unsupported').hidden"); assert.match(await js("document.querySelector('#unsupported').textContent"), /Windows/); assert.equal(await js("document.querySelector('#workspace').hidden"), true); await shot("d01-windows-unsupported"); console.log("chj.disks UI smoke (Windows unsupported view) passed"); return; } await until("!document.querySelector('#workspace').hidden && document.querySelectorAll('#diskMap .segment').length > 0"); if (language === "cs") assert.equal(await js("document.querySelector('h1').textContent"), "Správce disků"); await shot("d01-readonly"); const image = await fixture("create", RUN, "1024"); const loop = image.dev.slice(5); await js("document.querySelector('#privilegedButton').click()"); await untilWithSudo("document.querySelector('#privilegeBadge').classList.contains('ok')"); await until(`Array.from(document.querySelectorAll('#diskList li strong')).some((node) => node.textContent === '${image.dev}')`); // System disk: map widths follow the real sizes, rows show the root mount. assert.ok(await js("document.querySelector('#partitionRows').textContent.includes('/')")); const widths = await js("Array.from(document.querySelectorAll('#diskMap .segment')).map((node) => [node.getBoundingClientRect().width, Number(node.style.flexGrow)])"); const big = widths.reduce((best, entry) => entry[1] > best[1] ? entry : best); assert.ok(big[0] > 300, "the largest partition gets most of the map width"); await shot("d02-system-disk"); // Context menu on the system root offers no destructive action. await js("(() => { const seg = Array.from(document.querySelectorAll('#diskMap .segment')).find((node) => node.title.includes('/dev/')); const box = seg.getBoundingClientRect(); seg.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, clientX: box.left + 20, clientY: box.top + 20 })); })()"); await until("!document.querySelector('#contextMenu').hidden"); await shot("d03-context-menu"); await js("document.body.click()"); // Dedicated loop image: new GPT, then a new ext4 partition through the context menu and the queue. await js(`Array.from(document.querySelectorAll('#diskList li')).find((node) => node.querySelector('strong').textContent === '${image.dev}').click()`); await until(`document.querySelector('#diskTitle').textContent.startsWith('${image.dev}')`); await js("document.querySelector('#tableButton').click()"); await until("document.querySelector('#formDialog').open"); await js("document.querySelector('#formForm').requestSubmit()"); await untilWithSudo("!document.querySelector('#queuePanel').hidden && !document.querySelector('#previewBadge').hidden"); await until("document.querySelectorAll('#diskMap .segment.fs-free').length === 1"); await js("(() => { const seg = document.querySelector('#diskMap .segment.fs-free'); const box = seg.getBoundingClientRect(); seg.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, clientX: box.left + 30, clientY: box.top + 20 })); })()"); await until("!document.querySelector('#contextMenu').hidden"); await js("document.querySelector('#contextMenu button:not([disabled])').click()"); await until("document.querySelector('#formDialog').open && document.querySelector('#createSize')"); await js("document.querySelector('#createSize').value = '512'; document.querySelector('#createSizeUnit').value = 'MiB'; document.querySelector('#createLabel').value = 'chjui'; document.querySelector('#formForm').requestSubmit();"); await untilWithSudo("document.querySelectorAll('#queueList li').length === 2 && document.querySelector('#diskMap .segment.created')"); await shot("d04-queue-preview"); await js("document.querySelector('#queueApply').click()"); await untilWithSudo("document.querySelector('#applyDialog').open"); assert.ok(await js("document.querySelectorAll('#applyBody .step-list li').length >= 5"), "review shows the steps"); assert.ok(await js("document.querySelector('#applyBody .warnings').textContent.length > 50"), "review shows the risks"); await shot("d05-review"); await js("document.querySelector('#applyBackup').checked = true; document.querySelector('#applyForm').requestSubmit();"); await untilWithSudo("!document.querySelector('#jobPanel').hidden"); await until("document.querySelector('#jobState').classList.contains('succeeded')", 120000); await shot("d06-job-done"); await until(`document.querySelector('#partitionRows').textContent.includes('${loop}p1') && document.querySelector('#partitionRows').textContent.includes('chjui')`); assert.equal(await js("document.querySelectorAll('#diskMap .segment.fs-ext4').length"), 1, "new ext4 partition drawn"); await shot("d07-result"); const unexpected = errors.filter((message) => !/Autofill/.test(message)); assert.deepEqual(unexpected, [], "no console errors"); console.log("chj.disks UI smoke (Linux VM) passed"); } finally { if (fixtureUsed) { try { console.log("cleanup", JSON.stringify(await fixture("cleanup", RUN))); } catch (error) { console.error("cleanup failed", error.code || error.message); } } await sessionManager.disconnectAll("smoke"); } } run().then(() => app.exit(0), (error) => { console.error(error); app.exit(1); });