146 lines
5.9 KiB
JavaScript
146 lines
5.9 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 compile(file, overrides = {}, globals = {}) {
|
|
const exports = {}
|
|
vm.runInNewContext(ts.transpileModule(fs.readFileSync(require.resolve(file), 'utf8'), {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, jsx: ts.JsxEmit.ReactJSX }
|
|
}).outputText, { exports, require: name => overrides[name] ?? require(name), ...globals })
|
|
return exports
|
|
}
|
|
|
|
function nativeMenu() {
|
|
let template
|
|
let close
|
|
let clipboard = ''
|
|
let copies = 0
|
|
let pastes = 0
|
|
const frame = {}
|
|
const sender = { mainFrame: frame, isDestroyed: () => false, copy() { copies++ }, paste() { pastes++ } }
|
|
const event = { sender, senderFrame: frame }
|
|
const { showChatTextMenu } = compile('../src/main/chatTextMenu.ts', {
|
|
electron: {
|
|
BrowserWindow: { fromWebContents: () => ({ isDestroyed: () => false }) },
|
|
clipboard: { readText: () => clipboard },
|
|
Menu: { buildFromTemplate(items) {
|
|
template = items
|
|
return { popup(options) { close = options.callback } }
|
|
} }
|
|
}
|
|
})
|
|
return {
|
|
show: options => showChatTextMenu(event, options),
|
|
items: () => template,
|
|
close: () => close(),
|
|
setClipboard: text => { clipboard = text },
|
|
copies: () => copies, pastes: () => pastes, event
|
|
}
|
|
}
|
|
|
|
const labels = { copy: '복사', paste: '붙여넣기', clear: '대화내역 삭제' }
|
|
|
|
for (const canCopy of [false, true]) {
|
|
test(`message copy is enabled only with a selection (selected=${canCopy})`, async () => {
|
|
const menu = nativeMenu()
|
|
const result = menu.show({ kind: 'message', canCopy, canClear: true, labels })
|
|
assert.equal(menu.items()[0].enabled, canCopy)
|
|
assert.equal(menu.items()[0].label, '복사')
|
|
assert.equal(menu.items().some(item => item.label === '붙여넣기'), false)
|
|
menu.items()[0].click()
|
|
assert.equal(await result, null)
|
|
assert.equal(menu.copies(), canCopy ? 1 : 0)
|
|
})
|
|
}
|
|
|
|
for (const clipboard of ['', '복사해 둔 내용']) {
|
|
test(`input paste availability is read from the OS clipboard (empty=${!clipboard})`, async () => {
|
|
const menu = nativeMenu()
|
|
menu.setClipboard(clipboard)
|
|
const result = menu.show({ kind: 'input', canCopy: false, canPaste: true, labels })
|
|
assert.equal(menu.items().length, 2)
|
|
assert.equal(menu.items()[0].enabled, false)
|
|
assert.equal(menu.items()[1].label, '붙여넣기')
|
|
assert.equal(menu.items()[1].enabled, Boolean(clipboard))
|
|
menu.items()[1].click()
|
|
await result
|
|
assert.equal(menu.pastes(), clipboard ? 1 : 0)
|
|
})
|
|
}
|
|
|
|
test('read-only input never allows paste, even with clipboard text', async () => {
|
|
const menu = nativeMenu()
|
|
menu.setClipboard('text')
|
|
const result = menu.show({ kind: 'input', canCopy: true, canPaste: false, labels })
|
|
assert.equal(menu.items()[1].enabled, false)
|
|
menu.items()[1].click()
|
|
await result
|
|
assert.equal(menu.pastes(), 0)
|
|
})
|
|
|
|
test('paste rechecks an emptied clipboard before editing the input', async () => {
|
|
const menu = nativeMenu()
|
|
menu.setClipboard('text')
|
|
const result = menu.show({ kind: 'input', canCopy: false, canPaste: true, labels })
|
|
assert.equal(menu.items()[1].enabled, true)
|
|
menu.setClipboard('')
|
|
menu.items()[1].click()
|
|
await result
|
|
assert.equal(menu.pastes(), 0)
|
|
})
|
|
|
|
test('delete selection returns a confirmation request rather than deleting data', async () => {
|
|
const menu = nativeMenu()
|
|
const result = menu.show({ kind: 'message', canCopy: false, canClear: true, labels })
|
|
menu.items().find(item => item.label === '대화내역 삭제').click()
|
|
assert.equal(await result, 'clear')
|
|
})
|
|
|
|
test('closing a menu without choosing an action does nothing', async () => {
|
|
const menu = nativeMenu()
|
|
const result = menu.show({ kind: 'message', canCopy: true, labels })
|
|
menu.close()
|
|
assert.equal(await result, null)
|
|
assert.equal(menu.copies(), 0)
|
|
})
|
|
|
|
test('untrusted frames and invalid menu targets cannot open a native menu', () => {
|
|
const menu = nativeMenu()
|
|
assert.equal(menu.show({ kind: 'unknown', canCopy: true, labels }), null)
|
|
menu.event.senderFrame = {}
|
|
assert.equal(menu.show({ kind: 'input', canCopy: true, labels }), 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))]
|
|
}
|
|
|
|
for (const selected of [false, true]) {
|
|
test(`textarea opens the themed menu with a selection snapshot (selected=${selected})`, async () => {
|
|
let options
|
|
let prevented = false
|
|
let stopped = false
|
|
const { QaInputBox } = compile('../src/renderer/src/components/ChatWidgets.tsx', {
|
|
'../i18n': { t: key => key, useLocaleRender() {} }, './chatAttachments': {}, './ChatTextContextMenu': { ChatTextContextMenu: () => null }, './ChatImageAttachments': { ChatImageAttachments: () => null },
|
|
react: { ...React, useState: initial => [initial, value => { if (value?.kind === 'input') options = value }], useEffect() {}, useLayoutEffect() {}, useRef: () => ({ current: null }) }
|
|
}, { window: { chatTextMenu: { show: async input => { options = input; return null } } } })
|
|
const tree = QaInputBox.type({ dark: true, busy: false, disabled: false, onStop() {}, onSubmit() {} })
|
|
const textarea = elements(tree, node => node.type === 'textarea')[0]
|
|
textarea.props.onContextMenu({
|
|
currentTarget: { value: 'abcdef', selectionStart: 2, selectionEnd: selected ? 5 : 2, disabled: false, readOnly: false },
|
|
preventDefault() { prevented = true }, stopPropagation() { stopped = true }
|
|
})
|
|
await Promise.resolve()
|
|
assert.equal(options.kind, 'input')
|
|
assert.equal(options.text, selected ? 'cde' : '')
|
|
assert.equal(options.input.disabled, false)
|
|
assert.equal(options.selectionStart, 2)
|
|
assert.equal(prevented, true)
|
|
assert.equal(stopped, true)
|
|
})
|
|
}
|