mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-29 18:11:40 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b36dbc10ec | ||
|
|
81dce5a3e7 | ||
|
|
4e794d4af1 | ||
|
|
1205ef37ae | ||
|
|
f7d4cc14c2 | ||
|
|
1617ef8956 |
No files matched your search
@@ -824,3 +824,34 @@ test("AppLockOverlay keeps password fallback after system unlock failure", async
|
||||
dom.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("AppLockOverlay keeps a window drag region while lock controls stay interactive", async () => {
|
||||
const dom = installDomEnvironment();
|
||||
const renderer = await createDomRenderer(dom.document);
|
||||
|
||||
try {
|
||||
await renderer.render(
|
||||
React.createElement(
|
||||
I18nProvider,
|
||||
{ locale: "en" },
|
||||
React.createElement(AppLockOverlay, {
|
||||
locked: true,
|
||||
reason: "manual",
|
||||
onUnlock: async () => ({ ok: false, error: "incorrect" as const }),
|
||||
onResetAppLock: async () => {},
|
||||
}),
|
||||
),
|
||||
);
|
||||
await flushEffects();
|
||||
|
||||
const overlay = dom.document.querySelector("[data-app-lock-overlay]");
|
||||
const form = overlay?.querySelector("form");
|
||||
assert.ok(overlay);
|
||||
assert.ok(form);
|
||||
assert.match(overlay.getAttribute("class") ?? "", /\bapp-drag\b/);
|
||||
assert.match(form.getAttribute("class") ?? "", /\bapp-no-drag\b/);
|
||||
} finally {
|
||||
await renderer.unmount();
|
||||
dom.cleanup();
|
||||
}
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
@@ -10,3 +11,15 @@ test("getAppLockErrorMessageKey maps unlock errors to localized message keys", (
|
||||
assert.equal(getAppLockErrorMessageKey("incorrect"), "appLock.error.incorrectPassword");
|
||||
assert.equal(getAppLockErrorMessageKey(null), null);
|
||||
});
|
||||
|
||||
test("AppLockOverlay marks the backdrop as a window drag region", () => {
|
||||
const source = readFileSync(new URL("./AppLockOverlay.tsx", import.meta.url), "utf8");
|
||||
assert.match(
|
||||
source,
|
||||
/className="fixed inset-0 flex items-center justify-center bg-background px-6 text-foreground app-drag"/,
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/className="flex w-full max-w-\[360px\] flex-col items-center gap-5 app-no-drag"/,
|
||||
);
|
||||
});
|
||||
@@ -339,9 +339,11 @@ export const AppLockOverlay: React.FC<AppLockOverlayProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
// Overlay covers the title bar, and is the only chrome on a locked first
|
||||
// paint, so the backdrop owns the window drag region.
|
||||
<div
|
||||
ref={overlayRootRef}
|
||||
className="fixed inset-0 flex items-center justify-center bg-background px-6 text-foreground"
|
||||
className="fixed inset-0 flex items-center justify-center bg-background px-6 text-foreground app-drag"
|
||||
style={{ zIndex: 2147483647 }}
|
||||
role="dialog"
|
||||
data-state="open"
|
||||
@@ -350,7 +352,7 @@ export const AppLockOverlay: React.FC<AppLockOverlayProps> = ({
|
||||
aria-labelledby="app-lock-title"
|
||||
>
|
||||
<form
|
||||
className="flex w-full max-w-[360px] flex-col items-center gap-5"
|
||||
className="flex w-full max-w-[360px] flex-col items-center gap-5 app-no-drag"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<button
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { JSDOM } from "jsdom";
|
||||
|
||||
test("selecting an inline SFTP bookmark closes its popover", async () => {
|
||||
const dom = new JSDOM('<!doctype html><html><body><div id="root"></div></body></html>', {
|
||||
pretendToBeVisual: true,
|
||||
url: "http://localhost",
|
||||
});
|
||||
const window = dom.window;
|
||||
const previousGlobals = new Map<string, PropertyDescriptor | undefined>();
|
||||
const installGlobal = (key: string, value: unknown) => {
|
||||
previousGlobals.set(key, Object.getOwnPropertyDescriptor(globalThis, key));
|
||||
Object.defineProperty(globalThis, key, {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value,
|
||||
});
|
||||
};
|
||||
|
||||
class ResizeObserverStub {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
installGlobal("window", window);
|
||||
installGlobal("document", window.document);
|
||||
installGlobal("navigator", window.navigator);
|
||||
installGlobal("HTMLElement", window.HTMLElement);
|
||||
installGlobal("HTMLInputElement", window.HTMLInputElement);
|
||||
installGlobal("HTMLTextAreaElement", window.HTMLTextAreaElement);
|
||||
installGlobal("Element", window.Element);
|
||||
installGlobal("SVGElement", window.SVGElement);
|
||||
installGlobal("Node", window.Node);
|
||||
installGlobal("NodeFilter", window.NodeFilter);
|
||||
installGlobal("MutationObserver", window.MutationObserver);
|
||||
installGlobal("CustomEvent", window.CustomEvent);
|
||||
installGlobal("Event", window.Event);
|
||||
installGlobal("StorageEvent", window.StorageEvent);
|
||||
installGlobal("localStorage", window.localStorage);
|
||||
installGlobal("sessionStorage", window.sessionStorage);
|
||||
installGlobal("getComputedStyle", window.getComputedStyle.bind(window));
|
||||
installGlobal("requestAnimationFrame", window.requestAnimationFrame.bind(window));
|
||||
installGlobal("cancelAnimationFrame", window.cancelAnimationFrame.bind(window));
|
||||
installGlobal("ResizeObserver", ResizeObserverStub);
|
||||
installGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
||||
|
||||
const { default: React, act } = await import("react");
|
||||
const { createRoot } = await import("react-dom/client");
|
||||
const { SftpPaneToolbar } = await import("./SftpPaneToolbar.tsx");
|
||||
const rootNode = window.document.getElementById("root");
|
||||
assert.ok(rootNode);
|
||||
const root = createRoot(rootNode);
|
||||
const navigatedPaths: string[] = [];
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
root.render(
|
||||
React.createElement(SftpPaneToolbar, {
|
||||
t: (key) => ({
|
||||
"sftp.bookmark.list": "Bookmarked paths",
|
||||
"sftp.bookmark.remove": "Remove bookmark",
|
||||
"sftp.viewMode.switchToTree": "Switch to tree view",
|
||||
}[key] ?? key),
|
||||
pane: {
|
||||
id: "pane-1",
|
||||
connection: {
|
||||
id: "conn-1",
|
||||
hostId: "host-1",
|
||||
name: "Example",
|
||||
currentPath: "/home/app",
|
||||
homeDir: "/home/app",
|
||||
isLocal: false,
|
||||
},
|
||||
files: [],
|
||||
loading: false,
|
||||
reconnecting: false,
|
||||
error: null,
|
||||
connectionLogs: [],
|
||||
selectedFiles: new Set(),
|
||||
filter: "",
|
||||
filenameEncoding: "auto",
|
||||
showHiddenFiles: false,
|
||||
transferMutationToken: 0,
|
||||
},
|
||||
onNavigateTo: () => {},
|
||||
onSetFilter: () => {},
|
||||
onSetFilenameEncoding: () => {},
|
||||
onRefresh: () => {},
|
||||
showFilterBar: false,
|
||||
setShowFilterBar: () => {},
|
||||
filterInputRef: { current: null },
|
||||
isEditingPath: false,
|
||||
editingPathValue: "",
|
||||
setEditingPathValue: () => {},
|
||||
setShowPathSuggestions: () => {},
|
||||
showPathSuggestions: false,
|
||||
setPathSuggestionIndex: () => {},
|
||||
pathSuggestions: [],
|
||||
pathSuggestionIndex: -1,
|
||||
pathInputRef: { current: null },
|
||||
pathDropdownRef: { current: null },
|
||||
handlePathBlur: () => {},
|
||||
handlePathKeyDown: () => {},
|
||||
handlePathDoubleClick: () => {},
|
||||
handlePathSubmit: () => {},
|
||||
getNextUntitledName: () => "untitled",
|
||||
setNewFileName: () => {},
|
||||
setFileNameError: () => {},
|
||||
setShowNewFileDialog: () => {},
|
||||
setShowNewFolderDialog: () => {},
|
||||
setNewFolderName: () => {},
|
||||
bookmarks: [{ id: "bm-1", path: "/srv/www", label: "Web root" }],
|
||||
isCurrentPathBookmarked: false,
|
||||
onToggleBookmark: () => {},
|
||||
onAddGlobalBookmark: () => {},
|
||||
isCurrentPathGlobalBookmarked: false,
|
||||
onNavigateToBookmark: (path) => navigatedPaths.push(path),
|
||||
onDeleteBookmark: () => {},
|
||||
showHiddenFiles: false,
|
||||
onToggleShowHiddenFiles: () => {},
|
||||
viewMode: "list",
|
||||
onSetViewMode: () => {},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const trigger = window.document.querySelector<HTMLButtonElement>(
|
||||
'button[aria-label="Bookmarked paths"]',
|
||||
);
|
||||
assert.ok(trigger);
|
||||
await act(async () => trigger.click());
|
||||
|
||||
const bookmark = Array.from(window.document.querySelectorAll("button")).find((button) =>
|
||||
button.textContent?.includes("Web root"),
|
||||
);
|
||||
assert.ok(bookmark, "bookmark path should be visible after opening the popover");
|
||||
await act(async () => bookmark.click());
|
||||
|
||||
assert.deepEqual(navigatedPaths, ["/srv/www"]);
|
||||
assert.equal(
|
||||
Array.from(window.document.querySelectorAll("button")).some((button) =>
|
||||
button.textContent?.includes("Web root"),
|
||||
),
|
||||
false,
|
||||
"bookmark popover should close after selecting a path",
|
||||
);
|
||||
} finally {
|
||||
await act(async () => root.unmount());
|
||||
dom.window.close();
|
||||
for (const [key, descriptor] of previousGlobals) {
|
||||
if (descriptor) Object.defineProperty(globalThis, key, descriptor);
|
||||
else delete (globalThis as Record<string, unknown>)[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
SftpPaneToolbar,
|
||||
} from "./SftpPaneToolbar.tsx";
|
||||
import type { SftpPane } from "../../application/state/sftp/types.ts";
|
||||
import { Popover } from "../ui/popover.tsx";
|
||||
import { TooltipProvider } from "../ui/tooltip.tsx";
|
||||
|
||||
const toolbarSource = fs.readFileSync(
|
||||
@@ -495,14 +496,18 @@ test("bookmark list renders saved paths as selectable rows", () => {
|
||||
React.createElement(
|
||||
TooltipProvider,
|
||||
null,
|
||||
React.createElement(SftpBookmarkList, {
|
||||
bookmarks: [{ id: "bm-1", path: "/srv/www", label: "Web root" }],
|
||||
onNavigateToBookmark: () => {},
|
||||
onDeleteBookmark: () => {},
|
||||
t: (key: string) => ({
|
||||
"sftp.bookmark.remove": "Remove bookmark",
|
||||
}[key] ?? key),
|
||||
}),
|
||||
React.createElement(
|
||||
Popover,
|
||||
null,
|
||||
React.createElement(SftpBookmarkList, {
|
||||
bookmarks: [{ id: "bm-1", path: "/srv/www", label: "Web root" }],
|
||||
onNavigateToBookmark: () => {},
|
||||
onDeleteBookmark: () => {},
|
||||
t: (key: string) => ({
|
||||
"sftp.bookmark.remove": "Remove bookmark",
|
||||
}[key] ?? key),
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -251,14 +251,16 @@ export const SftpBookmarkList: React.FC<SftpBookmarkListProps> = ({
|
||||
)}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex-1 text-left text-xs truncate font-mono"
|
||||
onClick={() => onNavigateToBookmark(bm.path)}
|
||||
>
|
||||
{bm.label}
|
||||
<span className="ml-1.5 text-muted-foreground text-[10px]">{bm.path}</span>
|
||||
</button>
|
||||
<PopoverClose asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex-1 text-left text-xs truncate font-mono"
|
||||
onClick={() => onNavigateToBookmark(bm.path)}
|
||||
>
|
||||
{bm.label}
|
||||
<span className="ml-1.5 text-muted-foreground text-[10px]">{bm.path}</span>
|
||||
</button>
|
||||
</PopoverClose>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{bm.path}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
{
|
||||
version = "1.1.80";
|
||||
version = "1.1.81";
|
||||
|
||||
sources = {
|
||||
x86_64-linux = {
|
||||
appImageArch = "x86_64";
|
||||
hash = "sha256-2mwUopRx3XDLpXfePTT/MHfR30Sj2rFAzRy7Hn78C6c=";
|
||||
hash = "sha256-WkHgVhYfmqM24Zk37tAAa3uqUPtvh7jfN2nV3SQ+ong=";
|
||||
};
|
||||
aarch64-linux = {
|
||||
appImageArch = "arm64";
|
||||
hash = "sha256-a3uBC+6B2bnCG9US4smD9tTA7hISlsn8NnNBdWspRc4=";
|
||||
hash = "sha256-TMRFLvD0hW7rLtdg4ktMoV7hLv+4vHkucCTnvv61axQ=";
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user