const { test } = require('node:test') const assert = require('node:assert/strict') const fs = require('node:fs') const vm = require('node:vm') const ts = require('typescript') const React = require('react') const { renderToStaticMarkup } = require('react-dom/server') function compile(file, overrides = {}, globals = {}) { const exports = {} vm.runInNewContext(ts.transpileModule(fs.readFileSync(require.resolve(file), 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, jsx: ts.JsxEmit.ReactJSX } }).outputText, { exports, require: name => overrides[name] ?? require(name), ...globals }) return exports } test('custom menu clipboard IPC validates the main frame and rechecks paste availability', () => { let clipboard = '' let pastes = 0 const frame = {} const sender = { mainFrame: frame, isDestroyed: () => false, paste() { pastes++ } } const event = { sender, senderFrame: frame } const native = compile('../src/main/chatTextMenu.ts', { electron: { BrowserWindow: { fromWebContents: () => ({ isDestroyed: () => false }) }, clipboard: { readText: () => clipboard, writeText: text => { clipboard = text } } } }) assert.equal(native.readChatClipboardState(event).hasText, false) assert.equal(native.performChatClipboardAction(event, { action: 'paste' }), false) assert.equal(native.performChatClipboardAction(event, { action: 'copy', text: 'selected text' }), true) assert.equal(clipboard, 'selected text') assert.equal(native.readChatClipboardState(event).hasText, true) assert.equal(native.performChatClipboardAction(event, { action: 'paste' }), true) assert.equal(pastes, 1) clipboard = '' assert.equal(native.performChatClipboardAction(event, { action: 'paste' }), false) assert.equal(pastes, 1) event.senderFrame = {} assert.equal(native.performChatClipboardAction(event, { action: 'copy', text: 'blocked' }), false) assert.equal(native.readChatClipboardState(event).hasText, false) event.senderFrame = frame assert.equal(native.performChatClipboardAction(event, { action: 'copy', text: '' }), false) assert.equal(native.performChatClipboardAction(event, { action: 'copy', text: 'x'.repeat(5_000_001) }), false) assert.equal(native.performChatClipboardAction(event, { action: 'unknown' }), false) }) function renderMenu({ dark, kind = 'message', text = '', hasText = false, readOnly = false }) { const menu = { kind, x: 20, y: 30, text, input: { readOnly, disabled: false } } let state = 0 const { ChatTextContextMenu } = compile('../src/renderer/src/components/ChatTextContextMenu.tsx', { '../i18n': { t: key => key, useLocaleRender() {} }, 'react-dom': { createPortal: child => child }, react: { ...React, useEffect() {}, useLayoutEffect() {}, useRef: initial => ({ current: initial }), useState() { return [state++ === 0 ? { menu, hasText } : { x: 20, y: 30 }, () => {}] } } }, { document: { body: {} } }) return ChatTextContextMenu({ dark, menu, onClose() {}, onClear() {}, canClear: true }) } for (const dark of [true, false]) { test(`chat menu uses the terminal menu surface and item metrics (${dark ? 'dark' : 'light'})`, () => { const tree = renderMenu({ dark, text: 'selected' }) const html = renderToStaticMarkup(tree) assert.match(html, dark ? /bg-\[#252526\]/ : /bg-white/) assert.match(html, dark ? /enabled:hover:bg-\[#37373d\]/ : /enabled:hover:bg-\[#eef1f7\]/) assert.match(html, /rounded-md border py-1 text-\[12px\] font-medium/) assert.match(html, /flex h-8 w-full cursor-pointer/) assert.match(html, /role="menu"/) assert.match(html, /role="menuitem"/) assert.match(html, /대화내역 삭제/) assert.doesNotMatch(html, /붙여넣기/) }) } for (const hasText of [true, false]) { for (const readOnly of [true, false]) { test(`paste disabled state follows clipboard and input editability (clipboard=${hasText}, readOnly=${readOnly})`, () => { const tree = renderMenu({ dark: true, kind: 'input', hasText, readOnly }) const buttons = React.Children.toArray(tree.props.children).filter(child => child.type === 'button') assert.equal(buttons[0].props.disabled, true) assert.equal(buttons[1].props.disabled, !hasText || readOnly) assert.doesNotMatch(renderToStaticMarkup(tree), /대화내역 삭제/) }) } }