Files
lms-fe/scripts/test-chat-input-autosize.cjs
T
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

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)
})