[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { registerHooks } from "node:module";
|
||||
import test from "node:test";
|
||||
import React from "react";
|
||||
import { act, create, type ReactTestRenderer } from "react-test-renderer";
|
||||
|
||||
registerHooks({
|
||||
resolve(specifier, context, nextResolve) {
|
||||
if (specifier === "../Terminal" && context.parentURL?.endsWith("/TerminalLayerSupport.tsx")) {
|
||||
return {
|
||||
url: "data:text/javascript,export default function Terminal(){return null}",
|
||||
shortCircuit: true,
|
||||
};
|
||||
}
|
||||
return nextResolve(specifier, context);
|
||||
},
|
||||
load(url, context, nextLoad) {
|
||||
if (url.endsWith(".css")) {
|
||||
return { format: "module", source: "export default {};", shortCircuit: true };
|
||||
}
|
||||
return nextLoad(url, context);
|
||||
},
|
||||
});
|
||||
|
||||
const { useWorkspaceDetachPointerDrag } = await import("./TerminalLayerSupport");
|
||||
|
||||
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean })
|
||||
.IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
type Listener = EventListenerOrEventListenerObject;
|
||||
|
||||
function createDragDom() {
|
||||
const documentListeners = new Map<string, Set<Listener>>();
|
||||
const windowListeners = new Map<string, Set<Listener>>();
|
||||
const appendedElements = new Set<{ remove: () => void }>();
|
||||
|
||||
const addListener = (listeners: Map<string, Set<Listener>>, type: string, listener: Listener) => {
|
||||
const entries = listeners.get(type) ?? new Set<Listener>();
|
||||
entries.add(listener);
|
||||
listeners.set(type, entries);
|
||||
};
|
||||
const removeListener = (listeners: Map<string, Set<Listener>>, type: string, listener: Listener) => {
|
||||
listeners.get(type)?.delete(listener);
|
||||
};
|
||||
const dispatch = (listeners: Map<string, Set<Listener>>, type: string, event: Event) => {
|
||||
for (const listener of [...(listeners.get(type) ?? [])]) {
|
||||
if (typeof listener === "function") listener(event);
|
||||
else listener.handleEvent(event);
|
||||
}
|
||||
};
|
||||
|
||||
const fakeWindow = {
|
||||
addEventListener: (type: string, listener: Listener) => addListener(windowListeners, type, listener),
|
||||
removeEventListener: (type: string, listener: Listener) => removeListener(windowListeners, type, listener),
|
||||
};
|
||||
const fakeDocument = {
|
||||
body: {
|
||||
appendChild: (element: { remove: () => void }) => appendedElements.add(element),
|
||||
},
|
||||
createElement: () => {
|
||||
const element = {
|
||||
style: {} as Record<string, string>,
|
||||
textContent: "",
|
||||
remove: () => appendedElements.delete(element),
|
||||
};
|
||||
return element;
|
||||
},
|
||||
querySelector: () => null,
|
||||
addEventListener: (type: string, listener: Listener) => addListener(documentListeners, type, listener),
|
||||
removeEventListener: (type: string, listener: Listener) => removeListener(documentListeners, type, listener),
|
||||
defaultView: fakeWindow,
|
||||
};
|
||||
|
||||
return {
|
||||
document: fakeDocument as unknown as Document,
|
||||
documentListenerCount: (type: string) => documentListeners.get(type)?.size ?? 0,
|
||||
windowListenerCount: (type: string) => windowListeners.get(type)?.size ?? 0,
|
||||
dispatchDocument: (type: string, event: Event) => dispatch(documentListeners, type, event),
|
||||
dispatchWindow: (type: string, event: Event) => dispatch(windowListeners, type, event),
|
||||
appendedElements,
|
||||
};
|
||||
}
|
||||
|
||||
test("pointer drag cleanup runs when the pane unmounts without pointerup", async () => {
|
||||
const dom = createDragDom();
|
||||
let pointerDown: ((event: React.PointerEvent<HTMLElement>) => void) | null = null;
|
||||
let dragStartCount = 0;
|
||||
let dragEndCount = 0;
|
||||
|
||||
function Harness() {
|
||||
pointerDown = useWorkspaceDetachPointerDrag({
|
||||
inActiveWorkspace: true,
|
||||
session: { id: "session-1", workspaceId: "workspace-1" } as never,
|
||||
workspaceById: new Map(),
|
||||
onStartSessionDrag: () => { dragStartCount += 1; },
|
||||
onEndSessionDrag: () => { dragEndCount += 1; },
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(React.createElement(Harness));
|
||||
});
|
||||
|
||||
const pointerTarget = { ownerDocument: dom.document };
|
||||
await act(async () => {
|
||||
pointerDown!({
|
||||
button: 0,
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
currentTarget: pointerTarget,
|
||||
preventDefault: () => undefined,
|
||||
stopPropagation: () => undefined,
|
||||
} as unknown as React.PointerEvent<HTMLElement>);
|
||||
});
|
||||
|
||||
assert.equal(dom.documentListenerCount("pointermove"), 1);
|
||||
assert.equal(dom.documentListenerCount("pointerup"), 1);
|
||||
assert.equal(dom.documentListenerCount("pointercancel"), 1);
|
||||
assert.equal(dom.windowListenerCount("blur"), 1);
|
||||
assert.equal(dom.appendedElements.size, 0);
|
||||
assert.equal(dragStartCount, 0);
|
||||
|
||||
await act(async () => {
|
||||
renderer!.unmount();
|
||||
});
|
||||
|
||||
assert.equal(dom.documentListenerCount("pointermove"), 0);
|
||||
assert.equal(dom.documentListenerCount("pointerup"), 0);
|
||||
assert.equal(dom.documentListenerCount("pointercancel"), 0);
|
||||
assert.equal(dom.windowListenerCount("blur"), 0);
|
||||
assert.equal(dom.appendedElements.size, 0);
|
||||
assert.equal(dragEndCount, 0);
|
||||
});
|
||||
|
||||
test("unmount removes an active drag overlay and ends drag state", async () => {
|
||||
const dom = createDragDom();
|
||||
let pointerDown: ((event: React.PointerEvent<HTMLElement>) => void) | null = null;
|
||||
let dragStartCount = 0;
|
||||
let dragEndCount = 0;
|
||||
|
||||
function Harness() {
|
||||
pointerDown = useWorkspaceDetachPointerDrag({
|
||||
inActiveWorkspace: true,
|
||||
session: { id: "session-1", workspaceId: "workspace-1" } as never,
|
||||
workspaceById: new Map(),
|
||||
onStartSessionDrag: () => { dragStartCount += 1; },
|
||||
onEndSessionDrag: () => { dragEndCount += 1; },
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(React.createElement(Harness));
|
||||
});
|
||||
await act(async () => {
|
||||
pointerDown!({
|
||||
button: 0,
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
currentTarget: { ownerDocument: dom.document },
|
||||
preventDefault: () => undefined,
|
||||
stopPropagation: () => undefined,
|
||||
} as unknown as React.PointerEvent<HTMLElement>);
|
||||
dom.dispatchDocument("pointermove", { clientX: 10, clientY: 10 } as PointerEvent);
|
||||
});
|
||||
|
||||
assert.equal(dom.appendedElements.size, 2);
|
||||
assert.equal(dragStartCount, 1);
|
||||
|
||||
await act(async () => {
|
||||
renderer!.unmount();
|
||||
});
|
||||
|
||||
assert.equal(dom.documentListenerCount("pointermove"), 0);
|
||||
assert.equal(dom.documentListenerCount("pointerup"), 0);
|
||||
assert.equal(dom.documentListenerCount("pointercancel"), 0);
|
||||
assert.equal(dom.windowListenerCount("blur"), 0);
|
||||
assert.equal(dom.appendedElements.size, 0);
|
||||
assert.equal(dragEndCount, 1);
|
||||
});
|
||||
|
||||
test("all pointer drag exit paths share one idempotent cleanup", async () => {
|
||||
const dom = createDragDom();
|
||||
let pointerDown: ((event: React.PointerEvent<HTMLElement>) => void) | null = null;
|
||||
let dragStartCount = 0;
|
||||
let dragEndCount = 0;
|
||||
|
||||
function Harness() {
|
||||
pointerDown = useWorkspaceDetachPointerDrag({
|
||||
inActiveWorkspace: true,
|
||||
session: { id: "session-1", workspaceId: "workspace-1" } as never,
|
||||
workspaceById: new Map(),
|
||||
onStartSessionDrag: () => { dragStartCount += 1; },
|
||||
onEndSessionDrag: () => { dragEndCount += 1; },
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(React.createElement(Harness));
|
||||
});
|
||||
|
||||
const beginDrag = () => {
|
||||
pointerDown!({
|
||||
button: 0,
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
currentTarget: { ownerDocument: dom.document },
|
||||
preventDefault: () => undefined,
|
||||
stopPropagation: () => undefined,
|
||||
} as unknown as React.PointerEvent<HTMLElement>);
|
||||
dom.dispatchDocument("pointermove", { clientX: 10, clientY: 10 } as PointerEvent);
|
||||
};
|
||||
const assertClean = () => {
|
||||
assert.equal(dom.documentListenerCount("pointermove"), 0);
|
||||
assert.equal(dom.documentListenerCount("pointerup"), 0);
|
||||
assert.equal(dom.documentListenerCount("pointercancel"), 0);
|
||||
assert.equal(dom.windowListenerCount("blur"), 0);
|
||||
assert.equal(dom.appendedElements.size, 0);
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
beginDrag();
|
||||
dom.dispatchDocument("pointercancel", {} as PointerEvent);
|
||||
});
|
||||
assertClean();
|
||||
assert.equal(dragStartCount, 1);
|
||||
assert.equal(dragEndCount, 1);
|
||||
|
||||
await act(async () => {
|
||||
beginDrag();
|
||||
beginDrag();
|
||||
});
|
||||
assert.equal(dragStartCount, 3);
|
||||
assert.equal(dragEndCount, 2, "starting again must clean the previous drag exactly once");
|
||||
assert.equal(dom.documentListenerCount("pointermove"), 1);
|
||||
assert.equal(dom.appendedElements.size, 2);
|
||||
|
||||
await act(async () => {
|
||||
dom.dispatchWindow("blur", {} as Event);
|
||||
});
|
||||
assertClean();
|
||||
assert.equal(dragEndCount, 3);
|
||||
|
||||
await act(async () => {
|
||||
beginDrag();
|
||||
dom.dispatchDocument("pointerup", { clientX: 10, clientY: 10 } as PointerEvent);
|
||||
});
|
||||
assertClean();
|
||||
assert.equal(dragStartCount, 4);
|
||||
assert.equal(dragEndCount, 4);
|
||||
|
||||
await act(async () => {
|
||||
renderer!.unmount();
|
||||
});
|
||||
assert.equal(dragEndCount, 4, "unmount after cleanup must not end the drag twice");
|
||||
});
|
||||
Reference in New Issue
Block a user