8b501266ff
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.
146 lines
6.0 KiB
JavaScript
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)
|
|
})
|
|
}
|