137 lines
5.0 KiB
JavaScript
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)
|
|
})
|