CH-J Toolsمدیریت سرور از طریق SSH
منو
منبع منتشرشده

CH-J Tools

پوشه‌ها و فایل‌های یک نسخهٔ مشخص از برنامه را مرور کنید.

دانلود ZIP منبع
CH-J / Server Manager 406 فایل
core-20261024T135205Z-7c487d719de2 افزوده‌شده: · ساخته‌شده:
smokeWorkspaceUi.js افزوده‌شده: تاریخ ثبت نشده است ۲۶٫۰ KB · 301 خط
کد دانلود فایل
1"use strict";
2// Offline integration check: production renderer, preload, IPC, vault and bundled plugin runtime.
3const { app, BrowserWindow, ipcMain, protocol, clipboard } = require("electron");
4const assert = require("node:assert/strict");
5const fs = require("node:fs");
6const os = require("node:os");
7const path = require("node:path");
8const { createMainWindow, hardenSession } = require("../src/main/bootstrap/createMainWindow");
9const { registerCoreIpc } = require("../src/main/ipc/registerCoreIpc");
10const { ConfigStore } = require("../src/main/config/configStore");
11const { VaultStore } = require("../src/main/security/vaultStore");
12const { ProfileService } = require("../src/main/profiles/profileService");
13const { PluginRegistry } = require("../src/main/plugins/pluginRegistry");
14const { PluginRuntime } = require("../src/main/plugins/pluginRuntime");
15const { PluginService } = require("../src/main/plugins/pluginService");
16const { LocalHashService } = require("../src/main/hashing/localHashService");
17const { DiagnosticsService } = require("../src/main/diagnostics/diagnosticsService");
19const root = fs.mkdtempSync(path.join(os.tmpdir(), "chj-workspace-ui-"));
20app.setPath("userData", root);
21protocol.registerSchemesAsPrivileged([{ scheme: "chj-plugin", privileges: { standard: true, secure: true, supportFetchAPI: true } }]);
22let window, runtime, diagnostics, config, profiles;
23let nextImport, nextExport;
24const errors = [];
25const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
26const js = (source) => window.webContents.executeJavaScript(source);
27async function until(source, timeout = 10000) {
28 const started = Date.now();
29 while (Date.now() - started < timeout) { if (await js(`(() => { try { return (${source}); } catch { return false; } })()`)) return; await delay(40); }
30 throw new Error(`UI condition timed out: ${source}`);
32async function run() {
33 await app.whenReady(); hardenSession();
34 config = new ConfigStore(root); config.load(); config.update({ updates: { autoCheck: false } });
35 const vault = new VaultStore(root); await vault.create("ui-smoke-password"); profiles = new ProfileService(vault);
36 const first = profiles.save({ label: "NGINX", host: "NGINX.lan", username: "nginx", storePassword: true, password: "must-stay-in-vault" });
37 profiles.save({ label: "IPv6", host: "2001:db8::10", username: "admin", port: 2222 });
38 const registry = new PluginRegistry(root, null, { bundledRoot: path.join(__dirname, "../src/main/firstPartyPlugins") });
39 window = createMainWindow();
40 const watchErrors = (webContents) => webContents.on("console-message", (event) => { if (event.level === "error") { errors.push(event.message); console.error(event.message); } });
41 watchErrors(window.webContents);
42 runtime = new PluginRuntime({ BrowserWindow, registry, sessionManager: { list: () => [] }, localHashService: new LocalHashService(),
43 logService: { readTail: () => ({ text: "", size: 0, readAt: new Date().toISOString(), truncated: false }) },
44 preload: path.join(__dirname, "../src/preload/pluginPreload.js"), getMainWindow: () => window,
45 getLanguage: () => config.get().ui.language, isVaultUnlocked: () => vault.status().unlocked });
46 runtime.registerIpc(ipcMain);
47 const pluginService = new PluginService({ registry, runtime, configStore: config });
48 diagnostics = new DiagnosticsService({ storageRoot: root });
49 registerCoreIpc({ ipcMain, app, clipboard, configStore: config, vaultStore: vault, profileService: profiles,
50 pluginRegistry: registry, pluginService, pluginRuntime: runtime, diagnosticsService: diagnostics,
51 biometricService: { requireSensitiveAction: async () => {}, getStatus: () => ({ available: false, enabled: false, code: "BIOMETRIC_UNAVAILABLE" }) },
52 updateService: { getState: () => ({ currentVersion: "0.0.1", status: "idle", releases: [] }) }, getMainWindow: () => window,
53 dialog: { showOpenDialog: async () => nextImport ? { canceled: false, filePaths: [nextImport] } : { canceled: true, filePaths: [] },
54 showSaveDialog: async () => nextExport ? { canceled: false, filePath: nextExport } : { canceled: true } } });
55 await until('state.profiles.length === 2 && document.querySelectorAll("#pluginRows .plugin-row").length === 8');
56 assert.equal(await js('document.querySelector("#navigationEditToggle").checked'), false);
57 assert.equal(await js('document.querySelectorAll(".navigation-controls").length'), 0);
58 assert.equal(await js('[...document.querySelectorAll(".navigation-row")].some(row => row.draggable)'), false);
59 const dropTerminal = '{ const dataTransfer = new DataTransfer(); dataTransfer.setData("text/chj-navigation", "terminal"); document.querySelector("[data-nav-key=overview]").dispatchEvent(new DragEvent("drop", { dataTransfer, bubbles: true })); }';
60 await js(dropTerminal);
61 assert.deepEqual(config.get().ui.navigationOrder, []);
62 await js('document.querySelector("#navigationEditToggle").click();');
63 assert.equal(await js('[...document.querySelectorAll(".navigation-row")].every(row => row.draggable)'), true);
64 await js('{ const dataTransfer = new DataTransfer(); document.querySelector("[data-nav-key=terminal]").dispatchEvent(new DragEvent("dragstart", { dataTransfer, bubbles: true })); if (dataTransfer.getData("text/chj-navigation") !== "terminal") throw new Error("Missing drag payload"); document.querySelector("[data-nav-key=overview]").dispatchEvent(new DragEvent("drop", { dataTransfer, bubbles: true })); }');
65 await until('document.querySelector("#mainNavigation").firstElementChild.dataset.navKey === "terminal"');
66 assert.equal(new ConfigStore(root).load().ui.navigationOrder[0], "terminal");
67 await js(`showView('plugins'); document.querySelector('[data-plugin-id="chj.system-monitor"] .plugin-pin').click();`);
68 await until(`Boolean(document.querySelector('[data-nav-key="plugin:chj.system-monitor"]'))`);
69 await js('{ const dataTransfer = new DataTransfer(); dataTransfer.setData("text/chj-navigation", "plugin:chj.system-monitor"); document.querySelector("[data-nav-key=terminal]").dispatchEvent(new DragEvent("drop", { dataTransfer, bubbles: true })); }');
70 await until('document.querySelector("#mainNavigation").firstElementChild.dataset.navKey === "plugin:chj.system-monitor"');
71 await js('document.querySelector("#navigationEditToggle").click();');
72 await js(dropTerminal);
73 assert.equal(await js('document.querySelector("#mainNavigation").firstElementChild.dataset.navKey'), "plugin:chj.system-monitor");
74 await js(`document.querySelector('[data-plugin-id="chj.hash-checksum"] .plugin-visibility').click();`);
75 await until('!document.querySelector("#hashToolButton") && document.querySelector("#pluginRows .plugin-hidden .button").disabled');
76 assert.equal(await js('document.querySelector("#pluginRows .plugin-hidden .plugin-pin").disabled'), true);
77 await js(`document.querySelector('[data-plugin-id="chj.hash-checksum"] .plugin-visibility').click();`);
78 await until('document.querySelectorAll("#pluginRows .plugin-hidden").length === 0');
79 assert.equal(await js('document.querySelectorAll("#pluginRows select").length'), 0);
80 assert.equal(await js('document.querySelectorAll("#pluginRows details[open]").length'), 0);
81 assert.equal(await js('document.querySelector(".plugin-catalog-disclosure").open'), false);
82 await js('document.querySelector("#pluginRows details summary").click();');
83 assert.equal(await js('document.querySelector("#pluginRows details").open'), true);
84 await js('document.querySelector("#pluginRows details summary").click();');
85 // Open every packaged tool through the real sandboxed runtime, without SSH/network access.
86 for (const plugin of registry.listInstalled()) {
87 runtime.open(plugin.id);
88 const pluginWindow = runtime.windows.get(plugin.id); watchErrors(pluginWindow.webContents);
89 await new Promise((resolve, reject) => { pluginWindow.webContents.once("did-finish-load", resolve); pluginWindow.webContents.once("did-fail-load", (_e, code, description) => reject(new Error(`${plugin.id}: ${code} ${description}`))); });
90 await delay(180);
91 assert.equal(pluginWindow.webContents.getLastWebPreferences().nodeIntegration, false);
92 assert.equal(pluginWindow.webContents.getLastWebPreferences().sandbox, true);
93 assert.equal(await pluginWindow.webContents.executeJavaScript('typeof window.process'), "undefined");
94 assert.ok(await pluginWindow.webContents.executeJavaScript('Boolean(window.chjPlugin && document.body.textContent.length > 50)'));
95 runtime.close(plugin.id);
96 }
97 nextImport = path.join(root, "import.json");
98 fs.writeFileSync(nextImport, JSON.stringify({ format: "chj-server-profiles", version: 1, servers: [
99 { name: "NGINX import", host: "NGINX.lan", username: "nginx", port: 22, password: "never-import" },
100 { name: "Nový server", host: "192.168.10.100", username: "root", port: 22 }
101 ] }));
102 await js('showView("profiles"); document.querySelector("#importProfilesButton").click();');
103 await until('document.querySelector("#profileImportDialog").open');
104 assert.equal(profiles.list().length, 2);
105 assert.equal(await js('document.querySelectorAll("#profileImportRows tr").length'), 2);
106 await js('document.querySelector("#cancelProfileImport").click();');
107 assert.equal(profiles.list().length, 2);
108 await js('document.querySelector("#importProfilesButton").click();');
109 await until('document.querySelector("#profileImportDialog").open');
110 await js('document.querySelector("#profileDuplicatePolicy").value = "update"; document.querySelector("#confirmProfileImport").click();');
111 await until('!document.querySelector("#profileImportDialog").open && state.profiles.length === 3');
112 assert.equal(profiles.getStoredPassword(first.id), null);
113 for (const format of ["json", "csv"]) {
114 nextExport = path.join(root, `export.${format}`);
115 await js(`document.querySelector('#profileExportFormat').value = '${format}'; document.querySelector('#exportProfilesButton').click();`);
116 await until('!document.querySelector("#exportProfilesButton").disabled');
117 assert.doesNotMatch(fs.readFileSync(nextExport, "utf8"), /must-stay-in-vault|never-import|privateKey|token|password/i);
118 assert.match(fs.readFileSync(nextExport, "utf8"), /NGINX\.lan/);
119 }
120 nextExport = path.join(root, "selected.json");
121 await js('document.querySelector("#profileRows input").click(); document.querySelector("#exportSelectedProfiles").checked = true; document.querySelector("#profileExportFormat").value = "json"; document.querySelector("#exportProfilesButton").click();');
122 await until('!document.querySelector("#exportProfilesButton").disabled');
123 assert.equal(JSON.parse(fs.readFileSync(nextExport, "utf8")).servers.length, 1);
124 nextImport = path.join(root, "export.csv");
125 await js('document.querySelector("#importProfilesButton").click();');
126 await until('document.querySelector("#profileImportDialog").open');
127 assert.equal(await js('document.querySelectorAll("#profileImportRows tr").length'), 3);
128 await js('document.querySelector("#confirmProfileImport").click();');
129 await until('!document.querySelector("#profileImportDialog").open');
130 assert.equal(profiles.list().length, 3); // Default skip preserves existing profiles.
131 for (const language of ["cs", "de", "en"]) {
132 await js(`applyLanguage('${language}')`);
133 assert.equal(await js('document.querySelector("#profileImportTitle").textContent.includes("transfer.")'), false);
134 }
135 await window.webContents.reload();
136 await until('document.querySelector("#mainNavigation").firstElementChild.dataset.navKey === "plugin:chj.system-monitor" && state.profiles.length === 3');
137 assert.equal(await js('document.querySelector("#navigationEditToggle").checked'), false);
138 await sidebarChecks();
139 await iconChecks();
140 window.setSize(960, 720);
141 for (const view of ["plugins", "profiles"]) {
142 await js(`showView('${view}')`); await delay(80);
143 assert.equal(await js('document.documentElement.scrollWidth <= window.innerWidth'), true, `${view} must fit the minimum window width`);
144 }
145 const artifacts = process.env.CHJ_WORKSPACE_SMOKE_ARTIFACT_DIR;
146 if (artifacts) {
147 fs.mkdirSync(artifacts, { recursive: true }); window.setSize(1240, 900);
148 for (const view of ["plugins", "profiles"]) { await js(`showView('${view}')`); await delay(350); fs.writeFileSync(path.join(artifacts, `${view}.png`), (await window.webContents.capturePage()).toPNG()); }
149 }
150 assert.deepEqual(errors, []);
151 console.log("CHJ_WORKSPACE_UI_OK: locked/unlocked mouse reorder/persistence, compact plugin checkboxes, eight bundled sandboxed plugins, sidebar full/compact/split/hidden with resize, tooltips, shortcut and persistence, hide/show/pin, import preview/cancel/update, secret-free JSON/CSV, selected export, CZ/DE/EN, minimum window width");
153// Sidebar modes with real input events (mouse drag, hover, keyboard), persistence through ConfigStore and reload.
154async function sidebarChecks() {
155 const shots = process.env.CHJ_WORKSPACE_SMOKE_ARTIFACT_DIR;
156 const shot = async (name) => { if (shots) { fs.mkdirSync(shots, { recursive: true }); await delay(250); fs.writeFileSync(path.join(shots, `sidebar-${name}.png`), (await window.webContents.capturePage()).toPNG()); } };
157 const rect = (selector) => js(`(() => { const r = document.querySelector(${JSON.stringify(selector)}).getBoundingClientRect(); return { left: r.left, right: r.right, top: r.top, width: r.width, height: r.height }; })()`);
158 const send = (event) => window.webContents.sendInputEvent(event);
159 const settle = () => delay(260);
160 window.setSize(1240, 860); await settle();
161 await js("showView('overview')");
162 assert.deepEqual(config.get().ui.sidebar, { mode: "full", hidden: false, width: 248 });
163 assert.equal(Math.round((await rect("#sidebar")).width), 248);
164 // Drag the resize handle with the mouse.
165 const handle = await rect("#sidebarResizer");
166 const x = Math.round(handle.left + handle.width / 2); const y = Math.round(handle.top + 300);
167 send({ type: "mouseMove", x, y }); send({ type: "mouseDown", x, y, button: "left", clickCount: 1 });
168 for (let step = 1; step <= 10; step += 1) { send({ type: "mouseMove", x: x + step * 8, y, modifiers: ["leftButtonDown"] }); await delay(15); }
169 send({ type: "mouseUp", x: x + 80, y, button: "left", clickCount: 1 });
170 await until("state.config.ui.sidebar.width >= 320 && state.config.ui.sidebar.width <= 336");
171 await settle();
172 const widened = config.get().ui.sidebar.width;
173 assert.ok(Math.abs((await rect("#sidebar")).width - widened) <= 1, "sidebar follows the drag");
174 assert.ok(Math.abs((await rect("main")).left - widened) <= 1, "main content starts after the sidebar");
175 send({ type: "mouseDown", x: x + 80, y, button: "left", clickCount: 1 }); send({ type: "mouseMove", x: 1200, y, modifiers: ["leftButtonDown"] }); send({ type: "mouseUp", x: 1200, y, button: "left", clickCount: 1 });
176 await until("state.config.ui.sidebar.width === 420");
177 await js('document.querySelector("#sidebarResizer").focus()');
178 send({ type: "keyDown", keyCode: "Left" }); send({ type: "keyUp", keyCode: "Left" });
179 await until("state.config.ui.sidebar.width === 404");
180 await js('document.querySelector("#sidebarResizer").dispatchEvent(new MouseEvent("dblclick", { bubbles: true }))');
181 await until("state.config.ui.sidebar.width === 248");
182 await shot("full");
183 // Compact: icons only, tooltip with the full name on hover.
184 await js('document.querySelector(".sidebar-mode[data-sidebar-mode=compact]").click()');
185 await until('document.querySelector(".app-shell").dataset.sidebarMode === "compact"'); await settle();
186 assert.equal(Math.round((await rect("#sidebar")).width), 76);
187 assert.ok((await rect('[data-nav-key="diagnostics"] .nav-label')).width <= 1, "labels are visually hidden");
188 const diag = await rect('[data-nav-key="diagnostics"] .nav-button');
189 send({ type: "mouseMove", x: Math.round(diag.left + diag.width / 2), y: Math.round(diag.top + diag.height / 2) });
190 await until('!document.querySelector("#sidebarTooltip").hidden');
191 const expected = await js('document.querySelector(\'[data-nav-key="diagnostics"] .nav-label\').textContent');
192 assert.equal(await js('document.querySelector("#sidebarTooltip").textContent'), expected);
193 assert.ok((await rect("#sidebarTooltip")).left > (await rect("#sidebar")).right, "tooltip is next to the sidebar");
194 assert.equal(await js('getComputedStyle(document.querySelector("#sidebarTooltip")).pointerEvents'), "none");
195 await shot("compact");
196 send({ type: "mouseDown", x: Math.round(diag.left + diag.width / 2), y: Math.round(diag.top + diag.height / 2), button: "left", clickCount: 1 });
197 send({ type: "mouseUp", x: Math.round(diag.left + diag.width / 2), y: Math.round(diag.top + diag.height / 2), button: "left", clickCount: 1 });
198 await until('document.querySelector("[data-view-panel=diagnostics]").classList.contains("active")');
199 assert.equal(await js('document.querySelector("#sidebarTooltip").hidden'), true, "clicking hides the tooltip");
200 // Split: two icon columns; keyboard focus shows the tooltip too.
201 await js('document.querySelector(".sidebar-mode[data-sidebar-mode=split]").click()');
202 await until('document.querySelector(".app-shell").dataset.sidebarMode === "split"'); await settle();
203 assert.equal(Math.round((await rect("#sidebar")).width), 132);
204 const rows = await js('[...document.querySelectorAll("#mainNavigation .navigation-row")].map((row) => Math.round(row.getBoundingClientRect().top))');
205 assert.equal(rows[0], rows[1], "first two items share a row");
206 assert.ok(rows[2] > rows[0]);
207 // Real Tab key presses from the mode switch into the icon grid (works also when the window is not in the foreground).
208 await js('document.activeElement?.blur(); document.querySelector(".sidebar-mode[data-sidebar-mode=split]").focus()');
209 assert.equal(await js('document.activeElement.dataset.sidebarMode'), "split", "keyboard walk starts at the mode switch");
210 for (let press = 0; press < 6 && (press === 0 || !(await js('Boolean(document.activeElement.closest("#mainNavigation"))'))); press += 1) {
211 send({ type: "keyDown", keyCode: "Tab" }); send({ type: "keyUp", keyCode: "Tab" }); await delay(60);
212 }
213 assert.equal(await js('Boolean(document.activeElement.closest("#mainNavigation .nav-button"))'), true, "Tab reaches the icon grid");
214 // A window without OS focus (e.g. started from a non-interactive SSH session on Windows) drops element focus at once;
215 // the keyboard tooltip is only meaningful and checked in a focused window.
216 if (await js("document.hasFocus()")) {
217 await until('!document.querySelector("#sidebarTooltip").hidden');
218 assert.equal(await js('document.querySelector("#sidebarTooltip").textContent'), await js('document.activeElement.querySelector(".nav-label").textContent'));
219 } else console.log("NOTE: keyboard-focus tooltip not checked: the window has no OS focus in this session");
220 await shot("split");
221 // Hide with the menu button; the content takes the full width; the shortcut restores the last mode.
222 await js('document.querySelector("#sidebarToggle").click()');
223 await until('document.querySelector(".app-shell").classList.contains("sidebar-hidden")'); await settle();
224 assert.equal(await js('getComputedStyle(document.querySelector("#sidebar")).display'), "none");
225 const fullMain = await rect("main");
226 assert.ok(fullMain.left <= 1 && fullMain.width >= (await js("document.documentElement.clientWidth")) - 2, `main uses the whole width (${JSON.stringify(fullMain)})`);
227 assert.equal(await js('document.querySelector("#sidebarToggle").getAttribute("aria-expanded")'), "false");
228 await until("state.config.ui.sidebar.hidden === true");
229 assert.deepEqual(config.get().ui.sidebar, { mode: "split", hidden: true, width: 248 });
230 await shot("hidden");
231 send({ type: "keyDown", keyCode: "B", modifiers: [process.platform === "darwin" ? "meta" : "control", "shift"] });
232 send({ type: "keyUp", keyCode: "B", modifiers: [process.platform === "darwin" ? "meta" : "control", "shift"] });
233 await until('!document.querySelector(".app-shell").classList.contains("sidebar-hidden") && document.querySelector(".app-shell").dataset.sidebarMode === "split"');
234 // Persistence across a restart of the renderer.
235 await js('document.querySelector("#sidebarToggle").click()'); await until("state.config.ui.sidebar.hidden === true");
236 await window.webContents.reload();
237 await until('document.querySelector(".app-shell").dataset.sidebarMode === "split" && document.querySelector(".app-shell").classList.contains("sidebar-hidden")');
238 await js('document.querySelector("#sidebarToggle").click()');
239 await until('!document.querySelector(".app-shell").classList.contains("sidebar-hidden")');
240 await js('document.querySelector(".sidebar-mode[data-sidebar-mode=full]").click()');
241 await until("state.config.ui.sidebar.mode === 'full' && state.config.ui.sidebar.hidden === false");
242 assert.equal(await js('document.querySelector("#navigationEditToggle").checked'), false);
244// Navigation icons: shared SVG per stable ID in every mode and in the minimized-plugin taskbar.
245async function iconChecks() {
246 const shots = process.env.CHJ_WORKSPACE_SMOKE_ARTIFACT_DIR;
247 const shot = async (name) => { if (shots) { fs.mkdirSync(shots, { recursive: true }); await delay(250); fs.writeFileSync(path.join(shots, `icons-${name}.png`), (await window.webContents.capturePage()).toPNG()); } };
248 const send = (event) => window.webContents.sendInputEvent(event);
249 window.setSize(1240, 1000); await delay(200);
250 await js("showView('plugins')");
251 await js('[...document.querySelectorAll("#pluginRows .plugin-pin")].filter((pin) => !pin.checked && !pin.disabled).forEach((pin) => pin.click())');
252 await until('document.querySelectorAll("#mainNavigation .navigation-row").length === 15', 15000);
253 await js("showView('overview')");
254 const order = await js('[...document.querySelectorAll("#mainNavigation .navigation-row")].map((row) => row.dataset.navKey)');
255 const rows = await js(`[...document.querySelectorAll("#mainNavigation .navigation-row")].map((row) => { const svg = row.querySelector("svg.nav-icon"); const box = svg?.getBoundingClientRect(); return { key: row.dataset.navKey, icon: svg?.dataset.icon, expected: window.chjNavIcons.iconName(row.dataset.navKey), shapes: svg?.childElementCount || 0, w: box?.width, h: box?.height, glyph: row.querySelector(".nav-button > span:not(.nav-label)")?.textContent || "" }; })`);
256 for (const row of rows) {
257 assert.equal(row.icon, row.expected, `${row.key} icon`);
258 assert.notEqual(row.icon, "puzzle", `${row.key} has a specific icon`);
259 assert.ok(row.shapes > 0, `${row.key} SVG has shapes`);
260 assert.equal(row.glyph, "", `${row.key} has no old text glyph`);
261 assert.equal(Math.round(row.w), 22); assert.equal(Math.round(row.h), 22);
262 }
263 assert.equal(new Set(rows.map((row) => row.icon)).size, rows.length, "unique icons");
264 const fullButton = await js('document.querySelector("#mainNavigation .nav-button").getBoundingClientRect().height');
265 assert.ok(fullButton >= 48, `full-mode click area ${fullButton}`);
266 await shot("full");
267 for (const mode of ["compact", "split"]) {
268 await js(`document.querySelector(".sidebar-mode[data-sidebar-mode=${mode}]").click()`);
269 await until(`document.querySelector(".app-shell").dataset.sidebarMode === "${mode}"`); await delay(200);
270 const sizes = await js('[...document.querySelectorAll("#mainNavigation .navigation-row")].map((row) => { const b = row.querySelector(".nav-button").getBoundingClientRect(); const i = row.querySelector("svg.nav-icon").getBoundingClientRect(); return [Math.round(b.width), Math.round(b.height), Math.round(i.width), Math.round(i.height), Math.round(i.left + i.width / 2 - (b.left + b.width / 2)), Math.round(i.top + i.height / 2 - (b.top + b.height / 2)), Math.round(b.left), Math.round(b.top), Math.round(b.right)]; })');
271 const sidebarRight = await js('document.querySelector("#sidebar").getBoundingClientRect().right');
272 for (const [bw, bh, iw, ih, dx, dy, , , right] of sizes) {
273 assert.deepEqual([bw, bh, iw, ih], [52, 52, 26, 26], `${mode} sizes`);
274 assert.ok(Math.abs(dx) <= 1 && Math.abs(dy) <= 1, `${mode} icon centered`);
275 assert.ok(right <= sidebarRight, `${mode} button inside the sidebar`);
276 }
277 assert.equal(await js('(() => { const nav = document.querySelector("#mainNavigation"); return nav.scrollWidth <= nav.clientWidth; })()'), true, `${mode}: no horizontal overflow`);
278 const columns = new Set(sizes.map((entry) => entry[6]));
279 assert.equal(columns.size, mode === "split" ? 2 : 1, `${mode} columns`);
280 assert.deepEqual(await js('[...document.querySelectorAll("#mainNavigation .navigation-row")].map((row) => row.dataset.navKey)'), order, "custom order kept");
281 // Tooltip after ~300 ms hover, not immediately.
282 const target = await js('(() => { const r = document.querySelector("[data-nav-key=\'plugin:chj.disks\'] .nav-button").getBoundingClientRect(); return { x: Math.round(r.left + r.width / 2), y: Math.round(r.top + r.height / 2) }; })()');
283 send({ type: "mouseMove", x: 5, y: 5 }); await delay(60);
284 send({ type: "mouseMove", x: target.x, y: target.y }); await delay(120);
285 assert.equal(await js('document.querySelector("#sidebarTooltip").hidden'), true, "no tooltip before the delay");
286 await until('!document.querySelector("#sidebarTooltip").hidden', 2000);
287 assert.equal(await js('document.querySelector("#sidebarTooltip").textContent'), "Disk Manager");
288 await shot(mode);
289 send({ type: "mouseMove", x: 900, y: 500 }); await delay(60);
290 }
291 await js('document.querySelector(".sidebar-mode[data-sidebar-mode=full]").click()');
292 await until("state.config.ui.sidebar.mode === 'full'");
293 // Minimized-plugin taskbar uses the same icons; unknown plugins get the neutral fallback.
294 await js('renderPluginTaskbar([{ pluginId: "chj.disks", name: "Disk Manager", minimized: true }, { pluginId: "example.unknown", name: "Example", minimized: true }])');
295 assert.deepEqual(await js('[...document.querySelectorAll(".plugin-taskbar-item svg.nav-icon")].map((svg) => svg.dataset.icon)'), ["hard-drive", "puzzle"]);
296 await shot("taskbar");
297 await js("renderPluginTaskbar([])");
299function finish(code) { runtime?.closeAll(); diagnostics?.stopAll(); window?.destroy(); try { fs.rmSync(root, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch {} app.exit(code); }
300run().then(() => finish(0), (error) => { console.error(error); finish(1); });
301setTimeout(() => { console.error("Workspace UI smoke exceeded 120s"); finish(1); }, 120000).unref();
SHA-256faf1cbfb1c517bd2e811fd2035d1374dfb2c2656d642dc1361c9a839913bacec

CH-J Proprietary Software License 1.14 · کد منبع تحت CH-J Proprietary Software License 1.14 ارائه می‌شود. در دسترس بودن آن شرایط مجوز را تغییر نمی‌دهد و حق اضافه‌ای اعطا نمی‌کند.

SHA-256 بایگانی476ed8aef0f1e96a5acdf223127b6692acdf4492eeaf5d0816b6e2a634fbbda6
فایل‌ها