Files
NetMesh/infrastructure/monaco/monacoClipboardPaste.test.ts

208 lines
6.4 KiB
TypeScript
Raw Normal View History

import assert from 'node:assert/strict';
import test from 'node:test';
import {
buildMonacoPasteEdits,
isMonacoFindWidgetFocused,
isStillFocusedFindPasteTarget,
pasteForMonacoEditorCommand,
pasteTextIntoFocusedInput,
readClipboardTextWithFallbacks,
type FocusedTextInput,
} from './monacoClipboardPaste.ts';
function createMockTextInput(initialValue = ''): FocusedTextInput & {
selectionStart: number;
selectionEnd: number;
} {
let selectionStart = initialValue.length;
let selectionEnd = initialValue.length;
return {
value: initialValue,
get selectionStart() {
return selectionStart;
},
set selectionStart(value: number) {
selectionStart = value;
},
get selectionEnd() {
return selectionEnd;
},
set selectionEnd(value: number) {
selectionEnd = value;
},
focus() {},
setSelectionRange(start: number, end: number) {
selectionStart = start;
selectionEnd = end;
},
};
}
test('buildMonacoPasteEdits pastes full text at a single cursor', () => {
const edits = buildMonacoPasteEdits('hello\nworld', [
{ startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 },
]);
assert.deepEqual(edits, [
{
range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 },
text: 'hello\nworld',
forceMoveMarkers: true,
},
]);
});
test('buildMonacoPasteEdits spreads one line per cursor when counts match', () => {
const edits = buildMonacoPasteEdits('one\ntwo', [
{ startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 },
{ startLineNumber: 2, startColumn: 1, endLineNumber: 2, endColumn: 1 },
]);
assert.equal(edits.length, 2);
assert.equal(edits[0]?.text, 'one');
assert.equal(edits[1]?.text, 'two');
});
test('buildMonacoPasteEdits does not spread when line and cursor counts differ', () => {
const edits = buildMonacoPasteEdits('only-one-line', [
{ startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 },
{ startLineNumber: 2, startColumn: 1, endLineNumber: 2, endColumn: 1 },
]);
assert.equal(edits[0]?.text, 'only-one-line');
assert.equal(edits[1]?.text, 'only-one-line');
});
test('buildMonacoPasteEdits returns empty when there are no selections', () => {
assert.deepEqual(buildMonacoPasteEdits('text', []), []);
});
test('readClipboardTextWithFallbacks prefers navigator clipboard', async () => {
const text = await readClipboardTextWithFallbacks({
readNavigator: async () => 'from-navigator',
readBridge: async () => {
throw new Error('bridge should not run');
},
});
assert.equal(text, 'from-navigator');
});
test('readClipboardTextWithFallbacks uses bridge when navigator fails', async () => {
const text = await readClipboardTextWithFallbacks({
readNavigator: async () => {
throw new Error('denied');
},
readBridge: async () => 'from-bridge',
});
assert.equal(text, 'from-bridge');
});
test('readClipboardTextWithFallbacks returns null when both paths fail', async () => {
const text = await readClipboardTextWithFallbacks({
readNavigator: async () => {
throw new Error('denied');
},
readBridge: async () => {
throw new Error('unavailable');
},
});
assert.equal(text, null);
});
test('isMonacoFindWidgetFocused detects elements inside .find-widget', () => {
const findInput = {
closest: (selector: string) => (selector === '.find-widget' ? {} : null),
};
const otherInput = {
closest: () => null,
};
assert.equal(isMonacoFindWidgetFocused(findInput), true);
assert.equal(isMonacoFindWidgetFocused(otherInput), false);
assert.equal(isMonacoFindWidgetFocused(null), false);
});
test('pasteTextIntoFocusedInput replaces the current selection', () => {
const input = createMockTextInput('hello');
input.setSelectionRange(0, 5);
assert.equal(pasteTextIntoFocusedInput(input, 'world'), true);
assert.equal(input.value, 'world');
assert.equal(input.selectionStart, 5);
assert.equal(input.selectionEnd, 5);
});
test('pasteTextIntoFocusedInput rejects non-input targets', () => {
assert.equal(pasteTextIntoFocusedInput({ closest: () => ({}) }, 'x'), false);
});
test('pasteForMonacoEditorCommand pastes into find widget and skips editor body', async () => {
const input = Object.assign(createMockTextInput(''), {
closest: (selector: string) => (selector === '.find-widget' ? {} : null),
});
let bodyPasteCount = 0;
await pasteForMonacoEditorCommand({
activeElement: input,
readClipboardText: async () => 'search-me',
pasteIntoEditor: () => {
bodyPasteCount += 1;
},
});
assert.equal(input.value, 'search-me');
assert.equal(bodyPasteCount, 0);
});
test('pasteForMonacoEditorCommand falls through to editor body outside find widget', async () => {
let bodyPasteCount = 0;
await pasteForMonacoEditorCommand({
activeElement: null,
readClipboardText: async () => {
throw new Error('should not read clipboard for body path');
},
pasteIntoEditor: () => {
bodyPasteCount += 1;
},
});
assert.equal(bodyPasteCount, 1);
});
test('isStillFocusedFindPasteTarget rejects targets no longer inside the find widget', () => {
const leftFind = {
closest: () => null,
};
assert.equal(isStillFocusedFindPasteTarget(leftFind), false);
assert.equal(isStillFocusedFindPasteTarget(null), false);
});
test('pasteForMonacoEditorCommand aborts if focus leaves the find field mid clipboard read', async () => {
const input = Object.assign(createMockTextInput('keep'), {
closest: (selector: string) => (selector === '.find-widget' ? {} : null),
});
// Simulate a browser document where focus moved away during the await.
const previousDocument = (globalThis as { document?: Document }).document;
Object.defineProperty(globalThis, 'document', {
configurable: true,
value: { activeElement: { id: 'editor-body' } },
});
let bodyPasteCount = 0;
try {
await pasteForMonacoEditorCommand({
activeElement: input,
readClipboardText: async () => 'should-not-apply',
pasteIntoEditor: () => {
bodyPasteCount += 1;
},
});
} finally {
if (previousDocument === undefined) {
delete (globalThis as { document?: Document }).document;
} else {
Object.defineProperty(globalThis, 'document', {
configurable: true,
value: previousDocument,
});
}
}
assert.equal(input.value, 'keep');
assert.equal(bodyPasteCount, 0);
});