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 = [] let index = 0 let cleared = 0 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() {}, 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', visibleAiMessages: [{ id: '1', content: 'hello' }], visibleTeacherMessages: [], agentMessages: [], workspaceChatScrollRef: { current: null }, onClearConversation() { cleared++ }, ...overrides } return { render() { index = 0; return panel(props) }, cleared: () => cleared } } 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>/) 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\}/) })