import { test, afterEach } from "node:test"; import assert from "node:assert/strict"; import { mkdtemp, cp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { pathToFileURL, fileURLToPath } from "node:url"; import { setImmediate as flush } from "node:timers/promises"; import { JSDOM } from "jsdom"; const root = fileURLToPath(new URL("../", import.meta.url)); let cleanup; afterEach(async () => { if (cleanup) { await cleanup(); cleanup = null; } }); async function settle() { for (let i = 0; i < 6; i++) await flush(); } async function app(t) { t.mock.timers.enable({ apis: ["setTimeout"] }); const directory = await mkdtemp(path.join(tmpdir(), "harmony-ui-")); await cp(path.join(root, "curios/harmony-connector/ui"), directory, { recursive: true, }); await cp( path.dirname(fileURLToPath(import.meta.resolve("@trove/plugin-sdk"))), path.join(directory, "sdk"), { recursive: true }, ); const dom = new JSDOM( await readFile(path.join(directory, "index.html"), "utf8"), { url: "https://curio.test" }, ); globalThis.window = dom.window; globalThis.document = dom.window.document; const device = { id: "7:3", name: "daniel", model: "HUAWEI Pura 70 Ultra", manufacturer: "HUAWEI", }; const storages = [{ id: 1, name: "内部存储", free: 1e9, capacity: 2e9 }]; const model = { hdcAvailable: false, hdcConnected: true, confirmed: false, parameters: [], attached: true, connection: null, calls: [], job: null, busy: false, monitorWait: null, screenSession: 0, screenSequence: 0, screenGeometry: 1, screenControl: false, screenFrameWait: null, screenFrameError: null, screenUnchanged: false, openedUrls: [], selectedDirectory: "/fixture/folder", configureError: null, }; const { createMockBridge } = await import( pathToFileURL(path.join(directory, "sdk/index.js")) ); const handler = (method, fn) => [ `@self.${method}`, async (params) => { model.calls.push(method); model.parameters.push([method, structuredClone(params)]); return structuredClone(await fn(params)); }, ]; window.__TROVE_CONTEXT__ = { curioId: "dev.trove.harmony-connector", version: "0.1.0", instanceId: "test", platform: "macos", }; window.__TROVE_BRIDGE__ = createMockBridge( Object.fromEntries([ handler("hdc.screenStart", () => ({ session: ++model.screenSession, inputCapabilities: [ "click", "doubleClick", "longClick", "swipe", "drag", "text", "keyEvent", ], control: false, })), handler("hdc.screenStop", () => { model.screenControl = false; return { stopped: true }; }), handler("hdc.screenControl", ({ enabled }) => ({ control: (model.screenControl = enabled), })), handler("hdc.screenFrame", async () => { if (model.screenFrameWait) await model.screenFrameWait; if (model.screenFrameError) throw model.screenFrameError; if (model.screenUnchanged && model.screenSequence) return { unchanged: true, width: 1200, height: 2400, sequence: model.screenSequence, geometry: model.screenGeometry, control: model.screenControl, receivedFps: 0, mbps: 0, }; return { dataUrl: "data:image/jpeg;base64,/9j/2Q==", width: 1200, height: 2400, receivedFps: 27, mbps: 3.2, sequence: ++model.screenSequence, geometry: model.screenGeometry, control: model.screenControl, }; }), handler("hdc.screenInput", () => ({ executed: true })), handler("hdc.screenSave", () => ({ path: "/fixture/screen.jpg" })), handler("hdc.status", () => ({ busy: false, job: null })), handler("hdc.tool", () => ({ available: model.hdcAvailable, version: "HDC test", path: "/fixture/hdc", })), handler("hdc.configure", () => { if (model.configureError) throw model.configureError; model.hdcAvailable = true; return { available: true, version: "HDC test", path: "/fixture/hdc" }; }), [ "host.runtime.openUrl", ({ url }) => { model.openedUrls.push(url); return { ok: true }; }, ], handler("hdc.devices", () => ({ devices: model.hdcConnected ? [{ id: "phone", transport: "USB", state: "Connected" }] : [], })), handler("hdc.inspect", () => ({ capabilities: Object.fromEntries( ["apps", "launch", "logs", "report", "screenshot"].map((key) => [ key, { available: true }, ]), ), info: { text: "Test device" }, })), handler("hdc.apps", () => ({ apps: ["com.example.app"] })), handler("hdc.app", () => ({ bundle: "com.example.app", versionCode: 1, versionName: "1.0", debug: false, abilities: [{ name: "Home", module: "entry" }], details: {}, })), handler("hdc.package", () => ({ bundle: "com.example.app", versionCode: 2, versionName: "2.0", sha256: "confirmed-sha256", size: 100, })), handler("hdc.start", () => ({ id: 9 })), ["host.dialog.confirm", () => ({ confirmed: model.confirmed })], ["host.dialog.openDirectory", () => ({ path: model.selectedDirectory })], handler("info", () => ({ connection: model.connection, busy: false })), handler("devices", () => ({ devices: model.attached ? [device] : [] })), handler("connect", () => (model.connection = { device, storages })), handler("disconnect", () => { model.connection = null; return { disconnected: true, busy: model.busy }; }), handler("list", () => ({ entries: [ { id: 2, parent: 0xffffffff, storage: 1, name: "旅行.pdf", size: 100, folder: false, }, ], total: 1, })), handler("transferStatus", () => model.job), handler("transfer", () => { model.job = { id: 1, state: "running", sent: 0, total: 100 }; return { id: 1 }; }), ["host.dialog.openFile", () => ({ path: "/fixture/file.txt" })], handler("monitor", async () => { const snapshot = { connection: model.connection, devices: model.attached ? [device] : [], busy: model.busy, }; if (!model.attached) model.connection = null; if (model.monitorWait) { await model.monitorWait; return snapshot; } return { ...snapshot, connection: model.connection }; }), ]), ); cleanup = async () => { dom.window.dispatchEvent(new dom.window.Event("pagehide")); dom.window.close(); delete globalThis.window; delete globalThis.document; await rm(directory, { recursive: true, force: true }); }; await import(pathToFileURL(path.join(directory, "app.js"))); await settle(); const $ = (id) => document.getElementById(id); const click = async (id) => { $(id).click(); await settle(); }; const tick = async () => { t.mock.timers.tick(2500); await settle(); }; const connect = async () => { await click("scan"); await click("connect"); assert.equal($("browser").hidden, false); }; return { $, click, tick, connect, model }; } test("opening the plugin discovers already attached devices without clicking scan", async (t) => { const { $ } = await app(t); assert.equal($("device-picker").hidden, false); assert.equal($("device-select").options.length, 1); }); test("plugging in and unplugging refreshes the picker without manual scanning", async (t) => { const { $, tick, model } = await app(t); model.attached = false; await tick(); assert.equal($("device-picker").hidden, true); model.attached = true; await tick(); assert.equal($("device-picker").hidden, false); model.attached = false; await tick(); assert.equal($("device-select").options.length, 0); }); test("unplugging an idle connected phone clears the browser and connected badge", async (t) => { const { $, connect, tick, model } = await app(t); await connect(); model.attached = false; await tick(); assert.equal($("browser").hidden, true); assert.equal($("device-dot").classList.contains("connected"), false); assert.equal($("files").children.length, 0); assert.equal($("device-select").options.length, 0); }); test("manual disconnect clears files, filters and completed transfers", async (t) => { const { $, connect, click, tick, model } = await app(t); await connect(); await click("upload"); model.job = { id: 1, state: "completed", result: { name: "旅行.pdf", bytes: 100 }, }; await tick(); assert.equal($("transfer").hidden, false); $("search").value = "旅行"; await click("disconnect"); await tick(); assert.equal($("browser").hidden, true); assert.equal($("files").children.length, 0); assert.equal($("search").value, ""); assert.equal($("transfer").hidden, true); assert.equal($("footer-status").textContent.includes("已连接"), false); }); test("unplugging after a completed transfer clears the old success panel", async (t) => { const { $, connect, click, tick, model } = await app(t); await connect(); await click("upload"); model.job = { id: 1, state: "completed", result: { name: "旅行.pdf", bytes: 100 }, }; await tick(); assert.equal($("transfer").hidden, false); model.attached = false; await tick(); assert.equal($("transfer").hidden, true); }); test("connected device displays the friendly name and actual model separately", async (t) => { const { $, connect } = await app(t); await connect(); assert.equal($("device-name").textContent, "daniel"); assert.equal($("device-model").textContent, "HUAWEI Pura 70 Ultra"); assert.equal($("device-model").hidden, false); }); test("an in-flight USB snapshot cannot restore a manually disconnected session", async (t) => { const { $, connect, click, tick, model } = await app(t); await connect(); let resolve; model.monitorWait = new Promise((done) => { resolve = done; }); await tick(); const disconnect = click("disconnect"); resolve(); await disconnect; await settle(); assert.equal($("browser").hidden, true); assert.equal($("files").children.length, 0); }); test("closing the UI stops automatic scanning", async (t) => { const { tick, model } = await app(t); const before = model.calls.length; window.dispatchEvent(new window.Event("pagehide")); await tick(); await tick(); assert.equal(model.calls.length, before); }); test("USB disappearance during a transfer clears the browser while cleanup is pending", async (t) => { const { $, connect, click, tick, model } = await app(t); await connect(); await click("upload"); model.attached = false; model.busy = true; await tick(); assert.equal($("browser").hidden, true); assert.equal($("device-dot").classList.contains("connected"), false); assert.equal($("scan").disabled, true); assert.equal($("footer-status").textContent.includes("设备已断开"), true); }); test("manual disconnect keeps the UI responsive while USB cleanup is pending", async (t) => { const { $, connect, click, tick, model } = await app(t); await connect(); model.busy = true; const scans = model.calls.filter((method) => method === "devices").length; await click("disconnect"); assert.equal($("browser").hidden, true); assert.equal($("files").children.length, 0); assert.equal($("scan").disabled, true); assert.equal($("footer-status").textContent.includes("设备已断开"), true); assert.equal( model.calls.filter((method) => method === "devices").length, scans, ); model.busy = false; await tick(); assert.equal($("connect").disabled, false); assert.equal($("browser").hidden, true); }); test("whole folder upload uses the directory picker and exposes transfer metrics", async (t) => { const { $, click, connect, model, tick } = await app(t); await connect(); await click("upload-folder"); assert.equal( model.parameters.find(([name]) => name === "transfer")[1].path, "/fixture/folder", ); model.job.statistics = { bytesPerSecond: 1024, averageBytesPerSecond: 1024, elapsedSeconds: 2, remainingSeconds: 3, filesCompleted: 1, filesTotal: 4, currentFile: "nested/data.bin", }; await tick(); assert.match($("transfer-current").textContent, /nested\/data.bin/); assert.match($("transfer-metrics").textContent, /KB\/s/); }); test("management is optional and missing HDC does not disable file transfers", async (t) => { const { $, click, connect, model } = await app(t); assert.ok(!model.calls.some((name) => name.startsWith("hdc."))); await click("tab-management"); assert.match($("hdc-tool-status").textContent, /未发现/); assert.equal($("app-install").disabled, true); await click("tab-files"); await connect(); assert.equal($("browser").hidden, false); }); test("missing HDC guides official download and configuring an extracted toolkit", async (t) => { const { $, click, model } = await app(t); await click("tab-management"); assert.equal($("hdc-setup")?.hidden, false); assert.match($("hdc-setup").textContent, /Mac.*ARM64/); assert.match($("hdc-setup").textContent, /解压/); assert.match($("hdc-setup").textContent.replace(/\s+/g, " "), /USB 调试/); await click("hdc-download"); assert.deepEqual(model.openedUrls, [ "https://developer.huawei.com/consumer/cn/download/command-line-tools-for-hmos", ]); assert.equal($("hdc-setup").hidden, false); assert.ok(!model.calls.includes("hdc.devices")); await click("hdc-directory"); assert.deepEqual( model.parameters.find(([name]) => name === "hdc.configure")[1], { path: "/fixture/folder", }, ); assert.equal($("hdc-setup").hidden, true); assert.equal($("hdc-target").options.length, 1); }); test("refresh rechecks tool availability and clears stale devices if the tool disappears", async (t) => { const { $, click, model } = await app(t); await click("tab-management"); await click("hdc-refresh"); assert.ok(!model.calls.includes("hdc.devices")); model.hdcAvailable = true; await click("hdc-refresh"); assert.equal($("hdc-setup").hidden, true); assert.equal($("screen-start").disabled, false); model.hdcAvailable = false; await click("hdc-refresh"); assert.equal($("hdc-setup").hidden, false); assert.equal($("hdc-target").options.length, 0); assert.equal($("screen-start").disabled, true); }); test("cancelled or invalid toolkit selection preserves installation guidance", async (t) => { const { $, click, model } = await app(t); await click("tab-management"); model.selectedDirectory = null; await click("hdc-directory"); assert.ok(!model.calls.includes("hdc.configure")); model.selectedDirectory = "/fixture/wrong"; model.configureError = new Error("未找到 HDC,请选择解压后的工具目录。"); await click("hdc-directory"); assert.equal($("hdc-setup").hidden, false); assert.ok(!model.calls.includes("hdc.devices")); assert.match(document.body.textContent, /未找到 HDC/); }); test("application installation requires confirmation and pins the reviewed package hash", async (t) => { const { $, click, model } = await app(t); model.hdcAvailable = true; await click("tab-management"); await click("app-install"); assert.ok(!model.calls.includes("hdc.start")); model.confirmed = true; await click("app-install"); const params = model.parameters.find(([name]) => name === "hdc.start")[1]; assert.deepEqual(params, { kind: "install", target: "phone", path: "/fixture/file.txt", sha256: "confirmed-sha256", }); assert.equal($("app-install").disabled, true); }); test("device disconnect clears app details and prevents stale target operations", async (t) => { const { $, click, model } = await app(t); model.hdcAvailable = true; await click("tab-management"); $("apps-list").querySelector("button").click(); await settle(); assert.equal($("app-detail").hidden, false); assert.equal($("app-export").disabled, true); model.hdcConnected = false; t.mock.timers.tick(3500); await settle(); assert.equal($("app-detail").hidden, true); assert.equal($("app-install").disabled, true); assert.equal($("app-launch").disabled, true); }); test("screen viewing starts read-only, keeps refreshing and reports stream throughput", async (t) => { const { $, click, tick, model } = await app(t); model.hdcAvailable = true; await click("tab-management"); assert.equal($("screen-start").disabled, false); await click("screen-start"); await tick(); assert.equal($("screen-image").hidden, false); assert.match($("screen-status").textContent, /仅观看/); assert.match( $("screen-stats").textContent, /1200 × 2400.*接收 27 帧\/秒.*3.2 Mbps/, ); assert.equal($("screen-control").checked, false); assert.equal($("app-install").disabled, true); const before = model.screenSequence; await tick(); assert.ok(model.screenSequence > before); assert.ok(!model.calls.includes("hdc.screenInput")); }); test("screen text and navigation require enabled control and use the displayed session", async (t) => { const { $, click, tick, model } = await app(t); model.hdcAvailable = true; await click("tab-management"); await click("screen-start"); await tick(); await click("screen-control"); $("screen-text").value = "你好,daniel's phone"; $("screen-text").dispatchEvent(new window.Event("input")); await click("screen-send-text"); await tick(); const sent = model.parameters.find(([name]) => name === "hdc.screenInput")[1]; assert.deepEqual(sent, { session: 1, sequence: 1, geometry: 1, input: { kind: "text", text: "你好,daniel's phone" }, }); await click("screen-stop"); assert.equal($("screen-image").hidden, true); assert.equal($("screen-image").hasAttribute("src"), false); assert.equal($("screen-text").disabled, true); assert.equal($("app-install").disabled, false); }); test("an unchanged live screen preserves its image and does not count repeated frames", async (t) => { const { $, click, tick, model } = await app(t); model.hdcAvailable = true; await click("tab-management"); await click("screen-start"); await tick(); await click("screen-control"); const source = $("screen-image").src; const sequence = model.screenSequence; model.screenUnchanged = true; await tick(); await tick(); assert.equal($("screen-image").src, source); assert.equal($("screen-image").hidden, false); assert.equal(model.screenSequence, sequence); assert.equal($("screen-control").checked, true); assert.match($("screen-stats").textContent, /接收 0 帧\/秒/); assert.ok(!model.calls.includes("hdc.screenStop")); }); test("control does not wait behind a pending video frame", async (t) => { const { $, click, tick, model } = await app(t); model.hdcAvailable = true; await click("tab-management"); await click("screen-start"); await tick(); await click("screen-control"); let release; model.screenFrameWait = new Promise((resolve) => { release = resolve; }); await tick(); $("screen-inputs").querySelector('[data-screen-key="home"]').click(); await settle(); assert.ok( model.parameters.some( ([name, params]) => name === "hdc.screenInput" && params.input.key === "home", ), ); release(); model.screenFrameWait = null; await settle(); await click("screen-stop"); }); test("stopping during a pending frame discards that response and permits a fresh screen session", async (t) => { const { $, click, tick, model } = await app(t); model.hdcAvailable = true; await click("tab-management"); let release; model.screenFrameWait = new Promise((resolve) => { release = resolve; }); await click("screen-start"); await tick(); await click("screen-stop"); assert.equal($("screen-image").hidden, true); assert.ok(model.calls.includes("hdc.screenStop")); release(); model.screenFrameWait = null; await settle(); assert.equal($("screen-image").hasAttribute("src"), false); await click("screen-start"); await tick(); assert.equal(model.screenSession, 2); assert.equal($("screen-image").hidden, false); }); test("capture failure clears the screen and queued input instead of showing an old live frame", async (t) => { const { $, click, tick, model } = await app(t); model.hdcAvailable = true; await click("tab-management"); await click("screen-start"); await tick(); model.screenFrameError = new Error("Device disconnected"); await tick(); assert.equal($("screen-image").hidden, true); assert.match($("screen-status").textContent, /未开始/); assert.equal($("screen-control").checked, false); }); test("leaving management stops refresh and control; returning does not resume them", async (t) => { const { $, click, tick, model } = await app(t); model.hdcAvailable = true; await click("tab-management"); await click("screen-start"); await tick(); await click("screen-control"); await click("tab-files"); const before = model.screenSequence; await tick(); await click("tab-management"); await tick(); assert.equal(model.screenSequence, before); assert.equal($("screen-image").hidden, true); assert.equal($("screen-control").checked, false); }); test("rotation disables control and clears a gesture waiting to be sent", async (t) => { const { $, click, tick, model } = await app(t); model.hdcAvailable = true; await click("tab-management"); await click("screen-start"); await tick(); await click("screen-control"); model.screenGeometry++; model.screenControl = false; await tick(); assert.equal($("screen-control").checked, false); assert.match($("screen-input-status").textContent, /方向已变化/); });