feat(harmony): add folder transfers and HDC device management

This commit is contained in:
2026-09-10 11:56:09 +08:00
parent 6bdcfdb44e
commit 44d8cf5c92
30 changed files with 3881 additions and 299 deletions
+41 -1
View File
@@ -63,11 +63,42 @@ const entries = [
];
window.__TROVE_CONTEXT__ = {
curioId: "dev.trove.harmony-connector",
version: "0.1.1",
version: "0.2.0",
instanceId: "ui-fixture",
platform: "macos",
};
window.__TROVE_BRIDGE__ = createMockBridge({
"@self.hdc.status": () => ({ busy: false, job: null }),
"@self.hdc.tool": () => ({
available: true,
version: "HDC · 模拟数据",
path: "/fixture/official-sdk/hdc",
}),
"@self.hdc.devices": () => ({
devices: attached
? [{ id: "fixture-device", transport: "USB", state: "Connected" }]
: [],
}),
"@self.hdc.inspect": () => ({
capabilities: Object.fromEntries(
["apps", "launch", "screenshot", "logs", "report"].map((key) => [
key,
{ available: true },
]),
),
info: { text: "HUAWEI Pura 70 Ultra · 界面测试数据" },
}),
"@self.hdc.apps": () => ({
apps: ["com.example.gallery", "com.example.notes", "com.example.reader"],
}),
"@self.hdc.app": ({ bundle }) => ({
bundle,
versionName: "1.2.0",
versionCode: 12,
debug: bundle.endsWith("notes"),
abilities: [{ name: "Home", module: "entry" }],
details: {},
}),
"@self.info": () => ({ connection, busy: false }),
"@self.devices": () => ({ devices: attached ? [device] : [] }),
"@self.monitor": () => ({
@@ -104,6 +135,15 @@ window.__TROVE_BRIDGE__ = createMockBridge({
"@self.transferStatus": () => {
if (job?.state === "running") {
job.sent += 524288;
job.statistics = {
bytesPerSecond: 1310720,
averageBytesPerSecond: 1210720,
elapsedSeconds: job.sent / 1310720,
remainingSeconds: (job.total - job.sent) / 1310720,
filesTotal: 3,
filesCompleted: Math.floor((job.sent / job.total) * 3),
currentFile: "Documents/设计稿.zip",
};
if (job.sent >= job.total) {
job.state = "completed";
job.result = { name: "界面测试文件", bytes: job.total };
+108
View File
@@ -43,6 +43,10 @@ async function app(t) {
};
const storages = [{ id: 1, name: "内部存储", free: 1e9, capacity: 2e9 }];
const model = {
hdcAvailable: false,
hdcConnected: true,
confirmed: false,
parameters: [],
attached: true,
connection: null,
calls: [],
@@ -57,6 +61,7 @@ async function app(t) {
`@self.${method}`,
async (params) => {
model.calls.push(method);
model.parameters.push([method, structuredClone(params)]);
return structuredClone(await fn(params));
},
];
@@ -68,6 +73,45 @@ async function app(t) {
};
window.__TROVE_BRIDGE__ = createMockBridge(
Object.fromEntries([
handler("hdc.status", () => ({ busy: false, job: null })),
handler("hdc.tool", () => ({
available: model.hdcAvailable,
version: "HDC test",
path: "/fixture/hdc",
})),
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: "/fixture/folder" })],
handler("info", () => ({ connection: model.connection, busy: false })),
handler("devices", () => ({ devices: model.attached ? [device] : [] })),
handler("connect", () => (model.connection = { device, storages })),
@@ -257,3 +301,67 @@ test("manual disconnect keeps the UI responsive while USB cleanup is pending", a
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("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);
});