Skip to content

Commit 4d6a53a

Browse files
committed
fix(web): copy blank mobile terminal lines on long press
1 parent ff9adae commit 4d6a53a

2 files changed

Lines changed: 81 additions & 1 deletion

File tree

packages/web/src/features/terminal-panel/__tests__/xterm-host.test.tsx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7031,6 +7031,86 @@ describe("XtermHost", () => {
70317031
vi.useRealTimers();
70327032
});
70337033

7034+
it("mobile line copy still copies a resolved empty logical line on long press", async () => {
7035+
vi.useFakeTimers();
7036+
viewportMocks.viewport = "mobile";
7037+
7038+
const originalMatchMedia = window.matchMedia;
7039+
const writeText = vi.fn().mockResolvedValue(undefined);
7040+
const vibrate = vi.fn();
7041+
const store = createStore();
7042+
7043+
mockTerminal.buffer.active.viewportY = 14;
7044+
setMockBufferLines([[14, "", false]]);
7045+
7046+
window.matchMedia = vi.fn().mockImplementation((query: string) => ({
7047+
matches: query === "(pointer: coarse)",
7048+
media: query,
7049+
onchange: null,
7050+
addListener: vi.fn(),
7051+
removeListener: vi.fn(),
7052+
addEventListener: vi.fn(),
7053+
removeEventListener: vi.fn(),
7054+
dispatchEvent: vi.fn(),
7055+
})) as typeof window.matchMedia;
7056+
7057+
Object.defineProperty(navigator, "clipboard", {
7058+
configurable: true,
7059+
value: { writeText } satisfies Pick<Clipboard, "writeText">,
7060+
});
7061+
Object.defineProperty(navigator, "vibrate", {
7062+
configurable: true,
7063+
value: vibrate,
7064+
});
7065+
7066+
store.set(localeAtom, "en");
7067+
store.set(terminalPreferencesAtom, { copyOnSelect: true });
7068+
store.set(wsClientAtom, {
7069+
sendCommand: vi.fn().mockResolvedValue({ ok: true, data: { status: "ok" } }),
7070+
subscribe: vi.fn(() => () => {}),
7071+
getStatus: vi.fn(() => "connected"),
7072+
onStatus: vi.fn(() => () => {}),
7073+
sendTerminalInput: vi.fn().mockResolvedValue(undefined),
7074+
} as never);
7075+
7076+
const { container } = render(
7077+
<Provider store={store}>
7078+
<XtermHost terminalId="mobile-line-copy-empty-line-terminal" workspaceId="test-workspace" />
7079+
</Provider>
7080+
);
7081+
7082+
const host = container.querySelector(".xterm-host") as HTMLDivElement | null;
7083+
expect(host).toBeTruthy();
7084+
7085+
const rowsElement = document.createElement("div");
7086+
rowsElement.className = "xterm-rows";
7087+
rowsElement.innerHTML = "<div><span>&nbsp;</span></div>";
7088+
host!.appendChild(rowsElement);
7089+
7090+
const target = rowsElement.querySelector("span") as HTMLSpanElement;
7091+
dispatchTouchEvent(host!, "touchstart", [{ identifier: 1, clientX: 40, clientY: 120, target }]);
7092+
7093+
await act(async () => {
7094+
vi.advanceTimersByTime(500);
7095+
await Promise.resolve();
7096+
await Promise.resolve();
7097+
});
7098+
7099+
expect(writeText).toHaveBeenCalledWith("");
7100+
expect(store.get(toastsAtom)).toEqual(
7101+
expect.arrayContaining([
7102+
expect.objectContaining({
7103+
kind: "success",
7104+
title: "Copied current line",
7105+
}),
7106+
])
7107+
);
7108+
expect(vibrate).toHaveBeenCalledWith(10);
7109+
7110+
window.matchMedia = originalMatchMedia;
7111+
vi.useRealTimers();
7112+
});
7113+
70347114
it("mobile line copy does nothing when copy-on-select is disabled", async () => {
70357115
vi.useFakeTimers();
70367116
viewportMocks.viewport = "mobile";

packages/web/src/features/terminal-panel/views/shared/xterm-host.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -894,7 +894,7 @@ export function XtermHost({
894894
}
895895

896896
const lineText = getLogicalLineTextFromTouchTarget({ target, terminal });
897-
if (!lineText) {
897+
if (lineText === null) {
898898
return;
899899
}
900900

0 commit comments

Comments
 (0)