'use strict'
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const test = require('node:test')
const root = path.resolve(__dirname, '..', '..')
const read = relativePath => fs.readFileSync(
path.join(root, relativePath),
'utf8'
)
const { readStylesCss } = require('../helpers/css-source.js')
test('debug UI exposes the complete stepping and inspection surface', () => {
const html = read('index.html')
const requiredIds = [
'debug-pause',
'debug-step-into',
'debug-step',
'debug-step-out',
'debug-continue',
'debug-threads',
'debug-call-stack',
'debug-variable-list',
'debug-watch-list',
'debug-exception-list'
]
for (const id of requiredIds) {
assert.match(html, new RegExp(`id=["']${id}["']`))
}
})
test('debug shortcuts and renderer commands cover pause and stepping', () => {
const shortcuts = read('js/shortcuts.js')
const services = read('js/services.js')
const debugInterpreter = read('js/debug-interpreter.js')
assert.match(shortcuts, /id:\s*'stepInto'[\s\S]*?defaultValue:\s*'F7'/)
assert.match(shortcuts, /id:\s*'stepOut'[\s\S]*?defaultValue:\s*'Shift\+F8'/)
assert.match(shortcuts, /id:\s*'pause'[\s\S]*?scope:\s*'debug'/)
assert.match(debugInterpreter, /sendDebugCommand\('pause'/)
assert.match(debugInterpreter, /sendDebugCommand\('stepIn'/)
assert.match(debugInterpreter, /sendDebugCommand\('stepOut'/)
assert.match(
services,
/watchExpressions:\s*window\.debugWatchExpressions/
)
})
test('debug IPC exposes evaluate and exception breakpoint channels', () => {
const debugModule = read('js/main/cangjie-debug.js')
const preload = read('preload.js')
for (const channel of [
'cangjie-debug-command',
'cangjie-debug-evaluate',
'cangjie-debug-exceptions'
]) {
assert.match(debugModule, new RegExp(`ipcMain\\.handle\\('${channel}'`))
assert.match(preload, new RegExp(`'${channel}'`))
}
})
test('debug console input has a dedicated renderer-to-debuggee channel', () => {
const debugModule = read('js/main/cangjie-debug.js')
const preload = read('preload.js')
const services = read('js/services.js')
assert.match(debugModule, /ipcMain\.on\('cangjie-debug-stdin'/)
assert.match(preload, /'cangjie-debug-stdin'/)
assert.match(
services,
/window\.ipcRenderer\.send\('cangjie-debug-stdin',\s*\{\s*text\s*\}\)/
)
})
test('debug console REPL and setVariable expose a complete cross-process surface', () => {
const debugModule = read('js/main/cangjie-debug.js')
const preload = read('preload.js')
const client = read('js/cangjie-debug-client.js')
const html = read('index.html')
const services = read('js/services.js')
const debugInterpreter = read('js/debug-interpreter.js')
assert.match(debugModule, /ipcMain\.handle\('cangjie-debug-set-variable'/)
assert.match(preload, /'cangjie-debug-set-variable'/)
assert.match(
services,
/window\.ipcRenderer\.invoke\(\s*'cangjie-debug-set-variable'/
)
assert.match(debugModule, /payload\?\.context === 'repl'/)
assert.match(
client,
/async evaluate\(expression, frameId = this\.frameId, context = 'watch'\)/
)
assert.match(
client,
/async setVariable\(name, value, variablesReference = 0\)/
)
assert.match(
client,
/supportsSetVariable: this\.adapterCapabilities\?\.supportsSetVariable === true/
)
assert.match(html, /data-phase="debug"/)
assert.match(html, /id="svc-debug-console"/)
assert.match(html, /id="svc-debug-input"/)
assert.match(services, /window\.appendDebugConsole = function/)
assert.match(debugInterpreter, /context: 'repl'/)
assert.match(
debugInterpreter,
/window\.focusDebugConsoleInput = function/
)
assert.match(services, /window\.debugCanSetVariable === true/)
assert.match(
debugInterpreter,
/window\.debugCanSetVariable = !!\(\s*capabilities && capabilities\.supportsSetVariable === true/
)
})
test('function breakpoints expose a complete cross-process surface', () => {
const client = read('js/cangjie-debug-client.js')
const debugModule = read('js/main/cangjie-debug.js')
const services = read('js/services.js')
const debugInterpreter = read('js/debug-interpreter.js')
const conditional = read('js/conditional-breakpoints.js')
const html = read('index.html')
assert.match(
client,
/supportsFunctionBreakpoints: this\.adapterCapabilities\?\.supportsFunctionBreakpoints === true/
)
assert.match(client, /normalizeFunctionBreakpoints\(functions\)/)
assert.match(client, /async setFunctionBreakpoints\(functions = \[\]\)/)
assert.match(client, /this\.request\('setFunctionBreakpoints'/)
assert.match(
client,
/await this\.setFunctionBreakpoints\(functionBreakpoints\)/
)
assert.match(
client,
/action === 'continue'[\s\S]*?requestOptions\.functionBreakpoints[\s\S]*?setFunctionBreakpoints/
)
assert.match(
debugModule,
/functionBreakpoints:\s*Array\.isArray\(request\.functionBreakpoints\)/
)
assert.match(services, /window\.debugFunctionBreakpoints\s*=/)
assert.match(
services,
/window\.collectDebugFunctionBreakpoints = function/
)
assert.match(
services,
/window\.addDebugFunctionBreakpoint = addDebugFunctionBreakpoint/
)
assert.match(
services,
/window\.removeDebugFunctionBreakpoint = removeDebugFunctionBreakpoint/
)
assert.match(
debugInterpreter,
/functionBreakpoints:\s*window\.collectDebugFunctionBreakpoints/
)
assert.match(debugInterpreter, /commandPayload\.functionBreakpoints\s*=/)
assert.match(conditional, /Add Function Breakpoint/)
assert.match(conditional, /detectCangjieFunctionNameAtLine/)
assert.match(conditional, /window\.addDebugFunctionBreakpoint/)
assert.match(html, /id="debug-function-breakpoint-list"/)
})
test('large debug values expose an expanded JSON hover surface', () => {
const debugInterpreter = read('js/debug-interpreter.js')
const hoverProvider = read('js/monaco/debug-hover-provider-init.js')
const hoverLayout = read('js/monaco/debug-hover-layout-init.js')
const styles = readStylesCss()
assert.match(debugInterpreter, /formatDebugVariableJson/)
assert.match(debugInterpreter, /itemCount:\s*getDebugVariableCount\(variable\)/)
assert.match(debugInterpreter, /stage:\s*expanded\s*\?\s*'json'/)
assert.match(debugInterpreter, /language:\s*expanded\s*\?\s*'json'/)
assert.match(hoverProvider, /return null/)
assert.doesNotMatch(hoverProvider, /cangjie\.debug\.expandVariable/)
assert.doesNotMatch(debugInterpreter, /查看完整 JSON/)
assert.match(debugInterpreter, /window\.expandDebugVariableHover = function/)
assert.match(debugInterpreter, /window\.hideDebugVariableDetails = function/)
assert.match(hoverLayout, /window\.showDebugVariableDetails\(/)
assert.match(debugInterpreter, /className = 'debug-variable-popup'/)
assert.match(styles, /\.debug-variable-popup\b/)
})
test('visible debug pauses flush buffered target output once', () => {
const client = read('js/cangjie-debug-client.js')
assert.match(client, /await this\.flushDebuggeeOutput\(frame\.id\)/)
assert.match(
client,
/async flushDebuggeeOutput\(frameId\)[\s\S]*?print\("", flush: true\)/
)
assert.doesNotMatch(client, /findCangjieInputBreakpoints/)
assert.match(
client,
/hasPendingEvaluation\s*&&\s*\(category === 'stderr' \|\| \/\^\\s\*unknown error\\s\*\$\/i\.test\(outputText\)\)/
)
assert.match(
client,
/help: check if the .*\.cjo file of the package exists/
)
})
test('runtime-only frames never borrow the active user source line', () => {
const debugInterpreter = read('js/debug-interpreter.js')
const client = read('js/cangjie-debug-client.js')
assert.match(
debugInterpreter,
/if\s*\(!frame\s*\|\|\s*!frame\.source\s*\|\|\s*!frame\.source\.path\)[\s\S]*?clearDebugLine/
)
assert.doesNotMatch(
client,
/if\s*\(!userFrame\s*&&\s*stopBody\.reason\s*===\s*'step'\)[\s\S]*?this\.request\('continue'/
)
assert.match(
client,
/if\s*\(!userFrame\)\s*\{[\s\S]*?emit\('output'/
)
})
test('debug console preserves unterminated print output beside stdin', () => {
const debugInterpreter = read('js/debug-interpreter.js')
assert.match(debugInterpreter, /const endsWithNewline = normalized\.endsWith\('\\n'\)/)
assert.match(debugInterpreter, /partial:\s*isLast\s*&&\s*!endsWithNewline/)
})