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.
138 lines
5.1 KiB
JavaScript
138 lines
5.1 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 harness() {
|
|
let layoutEffects = []
|
|
let effects = []
|
|
let resizeCallback
|
|
let disconnected = false
|
|
const states = []
|
|
const refs = []
|
|
let stateIndex = 0
|
|
let refIndex = 0
|
|
const submissions = []
|
|
const input = {
|
|
style: {}, scrollHeight: 56,
|
|
getBoundingClientRect: () => ({ width: 300 })
|
|
}
|
|
const exports = {}
|
|
const source = fs.readFileSync(require.resolve('../src/renderer/src/components/ChatWidgets.tsx'), 'utf8')
|
|
vm.runInNewContext(ts.transpileModule(source, {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, jsx: ts.JsxEmit.ReactJSX }
|
|
}).outputText, {
|
|
exports,
|
|
window: { getComputedStyle: () => ({ lineHeight: '20px', paddingTop: '10px', paddingBottom: '10px', borderTopWidth: '0px', borderBottomWidth: '0px', minHeight: '56px' }) },
|
|
ResizeObserver: class {
|
|
constructor(callback) { resizeCallback = callback }
|
|
observe(element) { assert.equal(element, input) }
|
|
disconnect() { disconnected = true }
|
|
},
|
|
require(name) {
|
|
if (name === '../i18n') return { t: key => key, useLocaleRender() {} }
|
|
if (name === './chatAttachments') return {}
|
|
if (name === './ChatTextContextMenu') return { ChatTextContextMenu: () => null }
|
|
if (name === './ChatImageAttachments') return { ChatImageAttachments: () => null }
|
|
if (name === './ChatCodeBlock') return {}
|
|
if (name === 'react') return {
|
|
...React,
|
|
useState(initial) {
|
|
const index = stateIndex++
|
|
if (!(index in states)) states[index] = initial
|
|
return [states[index], value => { states[index] = value }]
|
|
},
|
|
useRef(initial) { return refs[refIndex++] ??= { current: initial } },
|
|
useEffect(callback) { effects.push(callback) },
|
|
useLayoutEffect(callback) { layoutEffects.push(callback) }
|
|
}
|
|
return require(name)
|
|
}
|
|
})
|
|
function find(node, type) {
|
|
if (!React.isValidElement(node)) return null
|
|
if (node.type === type) return node
|
|
return React.Children.toArray(node.props.children).map(child => find(child, type)).find(Boolean)
|
|
}
|
|
function render() {
|
|
stateIndex = 0
|
|
refIndex = 0
|
|
layoutEffects = []
|
|
effects = []
|
|
const tree = exports.QaInputBox.type({ dark: true, busy: false, disabled: false, onStop() {}, onSubmit: content => submissions.push(content) })
|
|
const textarea = find(tree, 'textarea')
|
|
textarea.props.ref.current = input
|
|
layoutEffects.forEach(callback => callback())
|
|
return textarea
|
|
}
|
|
return {
|
|
input, render, submissions,
|
|
mountEffects: () => effects.map(callback => callback()),
|
|
resize: width => resizeCallback([{ contentRect: { width } }]),
|
|
disconnected: () => disconnected
|
|
}
|
|
}
|
|
|
|
test('input grows to eight visible lines and scrolls only above the cap', () => {
|
|
const h = harness()
|
|
h.render()
|
|
assert.equal(h.input.style.height, '56px')
|
|
assert.equal(h.input.style.overflowY, 'hidden')
|
|
h.input.scrollHeight = 100
|
|
h.render()
|
|
assert.equal(h.input.style.height, '100px')
|
|
h.input.scrollHeight = 180
|
|
h.render()
|
|
assert.equal(h.input.style.height, '180px')
|
|
assert.equal(h.input.style.overflowY, 'hidden')
|
|
h.input.scrollHeight = 300
|
|
h.render()
|
|
assert.equal(h.input.style.height, '180px')
|
|
assert.equal(h.input.style.overflowY, 'auto')
|
|
})
|
|
|
|
test('deleting text and submitting restore the initial height', () => {
|
|
const h = harness()
|
|
let textarea = h.render()
|
|
textarea.props.onChange({ target: { value: 'question\nsecond line\nthird line' } })
|
|
h.input.scrollHeight = 80
|
|
textarea = h.render()
|
|
assert.equal(h.input.style.height, '80px')
|
|
let prevented = false
|
|
textarea.props.onKeyDown({ key: 'Enter', shiftKey: false, nativeEvent: {}, preventDefault() { prevented = true } })
|
|
assert.equal(prevented, true)
|
|
assert.equal(h.submissions.length, 1)
|
|
h.input.scrollHeight = 40
|
|
textarea = h.render()
|
|
assert.equal(textarea.props.value, '')
|
|
assert.equal(h.input.style.height, '56px')
|
|
assert.equal(h.input.style.overflowY, 'hidden')
|
|
})
|
|
|
|
test('Shift+Enter and IME composition do not submit', () => {
|
|
const h = harness()
|
|
let textarea = h.render()
|
|
textarea.props.onChange({ target: { value: 'question' } })
|
|
textarea = h.render()
|
|
const preventDefault = () => assert.fail('newline or composing Enter must not be intercepted')
|
|
textarea.props.onKeyDown({ key: 'Enter', shiftKey: true, nativeEvent: {}, preventDefault })
|
|
textarea.props.onKeyDown({ key: 'Enter', shiftKey: false, nativeEvent: { isComposing: true }, preventDefault })
|
|
assert.equal(h.submissions.length, 0)
|
|
})
|
|
|
|
test('panel width changes recalculate wrapped lines and observer disconnects on unmount', () => {
|
|
const h = harness()
|
|
h.render()
|
|
const cleanups = h.mountEffects()
|
|
h.input.scrollHeight = 120
|
|
h.resize(180)
|
|
assert.equal(h.input.style.height, '120px')
|
|
h.input.scrollHeight = 50
|
|
h.resize(400)
|
|
assert.equal(h.input.style.height, '56px')
|
|
cleanups.forEach(cleanup => cleanup?.())
|
|
assert.equal(h.disconnected(), true)
|
|
})
|