Files
lms-fe/scripts/test-chat-text-menu.cjs
chyoo 8b501266ff feat: improve agent responses and workspace UI
Add live agent progress, elapsed timing, verified code navigation and rich Markdown rendering. Preserve NanumSquare styling with missing glyph fallback, fix platform availability filtering and unify workspace scrollbar and visualization controls.
2026-10-19 17:06:18 +09:00

146 lines
6.0 KiB
JavaScript

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')
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
}
function nativeMenu() {
let template
let close
let clipboard = ''
let copies = 0
let pastes = 0
const frame = {}
const sender = { mainFrame: frame, isDestroyed: () => false, copy() { copies++ }, paste() { pastes++ } }
const event = { sender, senderFrame: frame }
const { showChatTextMenu } = compile('../src/main/chatTextMenu.ts', {
electron: {
BrowserWindow: { fromWebContents: () => ({ isDestroyed: () => false }) },
clipboard: { readText: () => clipboard },
Menu: { buildFromTemplate(items) {
template = items
return { popup(options) { close = options.callback } }
} }
}
})
return {
show: options => showChatTextMenu(event, options),
items: () => template,
close: () => close(),
setClipboard: text => { clipboard = text },
copies: () => copies, pastes: () => pastes, event
}
}
const labels = { copy: '복사', paste: '붙여넣기', clear: '대화내역 삭제' }
for (const canCopy of [false, true]) {
test(`message copy is enabled only with a selection (selected=${canCopy})`, async () => {
const menu = nativeMenu()
const result = menu.show({ kind: 'message', canCopy, canClear: true, labels })
assert.equal(menu.items()[0].enabled, canCopy)
assert.equal(menu.items()[0].label, '복사')
assert.equal(menu.items().some(item => item.label === '붙여넣기'), false)
menu.items()[0].click()
assert.equal(await result, null)
assert.equal(menu.copies(), canCopy ? 1 : 0)
})
}
for (const clipboard of ['', '복사해 둔 내용']) {
test(`input paste availability is read from the OS clipboard (empty=${!clipboard})`, async () => {
const menu = nativeMenu()
menu.setClipboard(clipboard)
const result = menu.show({ kind: 'input', canCopy: false, canPaste: true, labels })
assert.equal(menu.items().length, 2)
assert.equal(menu.items()[0].enabled, false)
assert.equal(menu.items()[1].label, '붙여넣기')
assert.equal(menu.items()[1].enabled, Boolean(clipboard))
menu.items()[1].click()
await result
assert.equal(menu.pastes(), clipboard ? 1 : 0)
})
}
test('read-only input never allows paste, even with clipboard text', async () => {
const menu = nativeMenu()
menu.setClipboard('text')
const result = menu.show({ kind: 'input', canCopy: true, canPaste: false, labels })
assert.equal(menu.items()[1].enabled, false)
menu.items()[1].click()
await result
assert.equal(menu.pastes(), 0)
})
test('paste rechecks an emptied clipboard before editing the input', async () => {
const menu = nativeMenu()
menu.setClipboard('text')
const result = menu.show({ kind: 'input', canCopy: false, canPaste: true, labels })
assert.equal(menu.items()[1].enabled, true)
menu.setClipboard('')
menu.items()[1].click()
await result
assert.equal(menu.pastes(), 0)
})
test('delete selection returns a confirmation request rather than deleting data', async () => {
const menu = nativeMenu()
const result = menu.show({ kind: 'message', canCopy: false, canClear: true, labels })
menu.items().find(item => item.label === '대화내역 삭제').click()
assert.equal(await result, 'clear')
})
test('closing a menu without choosing an action does nothing', async () => {
const menu = nativeMenu()
const result = menu.show({ kind: 'message', canCopy: true, labels })
menu.close()
assert.equal(await result, null)
assert.equal(menu.copies(), 0)
})
test('untrusted frames and invalid menu targets cannot open a native menu', () => {
const menu = nativeMenu()
assert.equal(menu.show({ kind: 'unknown', canCopy: true, labels }), null)
menu.event.senderFrame = {}
assert.equal(menu.show({ kind: 'input', canCopy: true, labels }), null)
})
function elements(root, predicate) {
if (!React.isValidElement(root)) return []
return [...(predicate(root) ? [root] : []), ...React.Children.toArray(root.props.children).flatMap(child => elements(child, predicate))]
}
for (const selected of [false, true]) {
test(`textarea opens the themed menu with a selection snapshot (selected=${selected})`, async () => {
let options
let prevented = false
let stopped = false
const { QaInputBox } = compile('../src/renderer/src/components/ChatWidgets.tsx', {
'../i18n': { t: key => key, useLocaleRender() {} }, './chatAttachments': {}, './ChatTextContextMenu': { ChatTextContextMenu: () => null }, './ChatImageAttachments': { ChatImageAttachments: () => null }, './ChatCodeBlock': {},
react: { ...React, useState: initial => [initial, value => { if (value?.kind === 'input') options = value }], useEffect() {}, useLayoutEffect() {}, useRef: () => ({ current: null }) }
}, { window: { chatTextMenu: { show: async input => { options = input; return null } } } })
const tree = QaInputBox.type({ dark: true, busy: false, disabled: false, onStop() {}, onSubmit() {} })
const textarea = elements(tree, node => node.type === 'textarea')[0]
textarea.props.onContextMenu({
currentTarget: { value: 'abcdef', selectionStart: 2, selectionEnd: selected ? 5 : 2, disabled: false, readOnly: false },
preventDefault() { prevented = true }, stopPropagation() { stopped = true }
})
await Promise.resolve()
assert.equal(options.kind, 'input')
assert.equal(options.text, selected ? 'cde' : '')
assert.equal(options.input.disabled, false)
assert.equal(options.selectionStart, 2)
assert.equal(prevented, true)
assert.equal(stopped, true)
})
}