Files
lms-fe/scripts/test-chat-input-autosize.cjs
T

137 lines
5.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 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 === '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)
})