148 lines
6.5 KiB
JavaScript
148 lines
6.5 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')
|
|
const { renderToStaticMarkup } = require('react-dom/server')
|
|
|
|
function compile(file, overrides = {}, globals = {}) {
|
|
const exports = {}
|
|
const source = fs.readFileSync(require.resolve(file), 'utf8')
|
|
const code = ts.transpileModule(source, {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, jsx: ts.JsxEmit.ReactJSX }
|
|
}).outputText
|
|
vm.runInNewContext(code, { exports, require: name => overrides[name] ?? require(name), ...globals })
|
|
return exports
|
|
}
|
|
|
|
const i18n = { t: key => key, useLocaleRender() {} }
|
|
const { ConfirmStatusModal } = compile('../src/renderer/src/components/AppStatusModals.tsx', { '../i18n': i18n })
|
|
const QaInputBox = () => null
|
|
|
|
function elements(root, predicate) {
|
|
if (!React.isValidElement(root)) return []
|
|
return [...(predicate(root) ? [root] : []), ...React.Children.toArray(root.props.children).flatMap(child => elements(child, predicate))]
|
|
}
|
|
|
|
function setup(overrides = {}) {
|
|
const state = []
|
|
const effects = []
|
|
let index = 0
|
|
let cleared = 0
|
|
const modeChanges = []
|
|
const panel = compile('../src/renderer/src/features/workspace/WorkspaceQaPanel.tsx', {
|
|
'../../i18n': i18n,
|
|
'../../components/AppStatusModals': { ConfirmStatusModal },
|
|
'../../components/ChatWidgets': { QaInputBox },
|
|
'react-dom': { createPortal: child => child },
|
|
react: {
|
|
...React,
|
|
useEffect(effect) { effects.push(effect) },
|
|
useState(initial) {
|
|
const slot = index++
|
|
if (!(slot in state)) state[slot] = initial
|
|
return [state[slot], value => { state[slot] = value }]
|
|
}
|
|
}
|
|
}, { document: { body: {} } }).default
|
|
const props = {
|
|
dark: true, workspaceActive: true, qaMode: 'ai', agentEnabled: true, visibleAiMessages: [{ id: '1', content: 'hello' }],
|
|
visibleTeacherMessages: [], agentMessages: [], workspaceChatScrollRef: { current: null },
|
|
onModeChange(mode) { modeChanges.push(mode) },
|
|
onClearConversation() { cleared++ }, ...overrides
|
|
}
|
|
return {
|
|
render() { index = 0; effects.length = 0; return panel(props) },
|
|
cleared: () => cleared,
|
|
runEffects() { effects.forEach(effect => effect()) },
|
|
modeChanges
|
|
}
|
|
}
|
|
|
|
for (const qaMode of ['ai', 'agent', 'teacher']) {
|
|
test(`${qaMode} history is deleted only after shared modal confirmation`, () => {
|
|
const harness = setup({ qaMode, agentMessages: [{ id: 'a', content: 'answer' }], visibleTeacherMessages: [{ id: 't', content: 'answer' }] })
|
|
const input = tree => elements(tree, node => node.type === QaInputBox)[0]
|
|
const modal = tree => elements(tree, node => node.type === ConfirmStatusModal)[0]
|
|
assert.equal(modal(harness.render()), undefined)
|
|
input(harness.render()).props.onClearConversation()
|
|
assert.equal(harness.cleared(), 0)
|
|
modal(harness.render()).props.onCancel()
|
|
assert.equal(modal(harness.render()), undefined)
|
|
assert.equal(harness.cleared(), 0)
|
|
input(harness.render()).props.onClearConversation()
|
|
modal(harness.render()).props.onConfirm()
|
|
assert.equal(harness.cleared(), 1)
|
|
assert.equal(modal(harness.render()), undefined)
|
|
})
|
|
}
|
|
|
|
test('context menu deletion also requests confirmation', () => {
|
|
const harness = setup()
|
|
const root = harness.render()
|
|
const area = elements(root, node => Boolean(node.props.onContextMenu))[0]
|
|
area.props.onContextMenu({ target: { closest: () => null }, preventDefault() {}, clientX: 10, clientY: 10 })
|
|
const clear = elements(harness.render(), node => node.type === 'button' && node.props.children === '대화내역 삭제')[0]
|
|
clear.props.onClick()
|
|
assert.equal(harness.cleared(), 0)
|
|
assert.equal(elements(harness.render(), node => node.type === ConfirmStatusModal).length, 1)
|
|
})
|
|
|
|
for (const props of [{ inputBusy: true }, { qaMessagesLoading: true }, { visibleAiMessages: [] }]) {
|
|
test(`does not open deletion confirmation when disabled: ${JSON.stringify(props)}`, () => {
|
|
const harness = setup(props)
|
|
const input = elements(harness.render(), node => node.type === QaInputBox)[0]
|
|
assert.equal(input.props.clearDisabled, true)
|
|
input.props.onClearConversation()
|
|
assert.equal(elements(harness.render(), node => node.type === ConfirmStatusModal).length, 0)
|
|
assert.equal(harness.cleared(), 0)
|
|
})
|
|
}
|
|
|
|
for (const dark of [false, true]) {
|
|
test(`shared confirmation renders cancel left and delete right (${dark ? 'dark' : 'light'})`, () => {
|
|
const harness = setup({ dark })
|
|
elements(harness.render(), node => node.type === QaInputBox)[0].props.onClearConversation()
|
|
const modal = elements(harness.render(), node => node.type === ConfirmStatusModal)[0]
|
|
assert.equal(modal.props.dark, dark)
|
|
assert.match(modal.props.overlayClassName, /fixed.*z-\[180\]/)
|
|
const html = renderToStaticMarkup(modal)
|
|
assert.match(html, /취소<\/button><button[^>]*>삭제<\/button>/)
|
|
assert.match(html, /bg-\[#d14b4b\]/)
|
|
})
|
|
}
|
|
|
|
test('input no longer opens a native confirmation dialog', () => {
|
|
const source = fs.readFileSync(require.resolve('../src/renderer/src/components/ChatWidgets.tsx'), 'utf8')
|
|
assert.doesNotMatch(source, /window\.confirm/)
|
|
assert.match(source, /onClick=\{onClearConversation\}/)
|
|
})
|
|
|
|
for (const dark of [false, true]) {
|
|
for (const agentEnabled of [false, true]) {
|
|
test(`agent availability controls tab count and layout (${dark ? 'dark' : 'light'}, enabled=${agentEnabled})`, () => {
|
|
const harness = setup({ dark, agentEnabled })
|
|
const grids = elements(harness.render(), node => node.props.className?.includes('mt-3 grid'))
|
|
assert.equal(grids.length, 1)
|
|
const tabs = elements(grids[0], node => node.type === 'button')
|
|
assert.deepEqual(tabs.map(tab => tab.props.children[1].props.children), agentEnabled ? ['AI', '에이전트', '강사'] : ['AI', '강사'])
|
|
assert.ok(grids[0].props.className.includes(agentEnabled ? 'grid-cols-3' : 'grid-cols-2'))
|
|
})
|
|
}
|
|
}
|
|
|
|
test('returns to AI when the selected agent is disabled after status loading', () => {
|
|
const harness = setup({ qaMode: 'agent', agentEnabled: false, agentStatusLoading: false, minimized: true })
|
|
harness.render()
|
|
harness.runEffects()
|
|
assert.deepEqual(harness.modeChanges, ['ai'])
|
|
})
|
|
|
|
test('does not reset the selected agent while status is still loading', () => {
|
|
const harness = setup({ qaMode: 'agent', agentEnabled: false, agentStatusLoading: true, minimized: true })
|
|
harness.render()
|
|
harness.runEffects()
|
|
assert.deepEqual(harness.modeChanges, [])
|
|
})
|