feat: improve chat input, themed menus and agent proposal cancellation
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
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), /대화내역 삭제/)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user