631 lines
16 KiB
JavaScript
631 lines
16 KiB
JavaScript
import test from 'brittle'
|
|
import { attachBareOsTuiSdk, bareOsTuiEnabled } from './lib/services/bare-os-tui-sdk.js'
|
|
import { BARE_OS_TUI_SDK_SOURCE } from './lib/services/bare-os-tui-sdk.data.mjs'
|
|
import { buildBareOsRuntimeCaps } from './lib/ctx/bare-os-runtime-caps.js'
|
|
|
|
function makeStream(isTTY, writes) {
|
|
const listeners = { data: [], resize: [] }
|
|
return {
|
|
isTTY: !!isTTY,
|
|
raw: false,
|
|
columns: 80,
|
|
rows: 24,
|
|
chunks: writes,
|
|
write(s) {
|
|
writes.push(String(s))
|
|
},
|
|
setRawMode(v) {
|
|
this.raw = !!v
|
|
},
|
|
resume() {},
|
|
pause() {},
|
|
on(ev, fn) {
|
|
if (!listeners[ev]) listeners[ev] = []
|
|
listeners[ev].push(fn)
|
|
},
|
|
removeListener(ev, fn) {
|
|
const list = listeners[ev]
|
|
if (!list) return
|
|
const i = list.indexOf(fn)
|
|
if (i >= 0) list.splice(i, 1)
|
|
},
|
|
emit(ev, data) {
|
|
const list = listeners[ev] || []
|
|
for (const fn of list) fn(data)
|
|
}
|
|
}
|
|
}
|
|
|
|
function makeCtx(env, opts) {
|
|
const writes = []
|
|
const tty = opts && opts.tty === false ? false : true
|
|
const stdin = makeStream(tty, writes)
|
|
const stdout = makeStream(tty, writes)
|
|
let suspends = 0
|
|
let resumes = 0
|
|
return {
|
|
env: env || {},
|
|
console: { error() {}, log() {} },
|
|
replStdin: stdin,
|
|
replStdout: stdout,
|
|
suspendReplForSubprocess() {
|
|
suspends++
|
|
},
|
|
resumeReplAfterSubprocess() {
|
|
resumes++
|
|
},
|
|
_counts: () => ({
|
|
get suspends() {
|
|
return suspends
|
|
},
|
|
get resumes() {
|
|
return resumes
|
|
},
|
|
writes,
|
|
stdin,
|
|
stdout
|
|
})
|
|
}
|
|
}
|
|
|
|
test('bareOsTuiEnabled defaults on', (t) => {
|
|
t.ok(bareOsTuiEnabled({}))
|
|
t.ok(bareOsTuiEnabled({ BARE_OS_TUI: '1' }))
|
|
t.absent(bareOsTuiEnabled({ BARE_OS_TUI: '0' }))
|
|
t.absent(bareOsTuiEnabled({ BARE_OS_TUI: 'false' }))
|
|
})
|
|
|
|
test('runtime cap tuiSdk follows BARE_OS_TUI', (t) => {
|
|
const on = buildBareOsRuntimeCaps({})
|
|
t.ok(on.features.tuiSdk)
|
|
const off = buildBareOsRuntimeCaps({ BARE_OS_TUI: '0' })
|
|
t.absent(off.features.tuiSdk)
|
|
})
|
|
|
|
test('attachBareOsTuiSdk installs ctx.tui and ctx.sdk', (t) => {
|
|
t.ok(
|
|
typeof BARE_OS_TUI_SDK_SOURCE === 'string' &&
|
|
BARE_OS_TUI_SDK_SOURCE.length > 100
|
|
)
|
|
const ctx = makeCtx({})
|
|
t.ok(attachBareOsTuiSdk(ctx))
|
|
t.ok(ctx.tui)
|
|
t.ok(ctx.sdk)
|
|
t.is(ctx.sdk.tui, ctx.tui)
|
|
t.is(ctx.tui.version, '1.3.0')
|
|
t.ok(ctx.tui.isTTY())
|
|
t.is(ctx.tui.size().width, 80)
|
|
t.is(ctx.tui.size().height, 24)
|
|
t.ok(ctx.tui.ansi.enterAltScreen.indexOf('1049h') >= 0)
|
|
})
|
|
|
|
test('attachBareOsTuiSdk skipped when BARE_OS_TUI=0', (t) => {
|
|
const ctx = makeCtx({ BARE_OS_TUI: '0' })
|
|
t.absent(attachBareOsTuiSdk(ctx))
|
|
t.absent(ctx.tui)
|
|
})
|
|
|
|
test('attachBareOsTuiSdk honors bareOsIsCtxMethodAllowed', (t) => {
|
|
const ctx = makeCtx({})
|
|
ctx.bareOsIsCtxMethodAllowed = (name) => name !== 'tui'
|
|
t.absent(attachBareOsTuiSdk(ctx))
|
|
t.absent(ctx.tui)
|
|
})
|
|
|
|
test('decoder: arrows, ctrl+c, enter, utf-8', (t) => {
|
|
const ctx = makeCtx({})
|
|
attachBareOsTuiSdk(ctx)
|
|
const up = ctx.tui.decode('\x1b[A')
|
|
t.is(up.length, 1)
|
|
t.ok(ctx.tui.key.matches(up[0], 'up'))
|
|
const cc = ctx.tui.decode('\x03')
|
|
t.ok(ctx.tui.key.matches(cc[0], 'ctrl+c'))
|
|
const ent = ctx.tui.decode('\r')
|
|
t.ok(ctx.tui.key.matches(ent[0], 'enter'))
|
|
const hi = ctx.tui.decode('é')
|
|
t.is(hi[0].name, 'é')
|
|
})
|
|
|
|
test('decoder: SGR mouse and bracketed paste', (t) => {
|
|
const ctx = makeCtx({})
|
|
attachBareOsTuiSdk(ctx)
|
|
const mouse = ctx.tui.decode('\x1b[<0;10;5M')
|
|
t.is(mouse[0].type, 'mouse')
|
|
t.is(mouse[0].action, 'press')
|
|
t.is(mouse[0].button, 'left')
|
|
t.is(mouse[0].x, 10)
|
|
t.is(mouse[0].y, 5)
|
|
const paste = ctx.tui.decode('\x1b[200~hello\x1b[201~')
|
|
t.is(paste[0].type, 'paste')
|
|
t.is(paste[0].text, 'hello')
|
|
})
|
|
|
|
test('decoder: incomplete ESC then flushEscape', (t) => {
|
|
const ctx = makeCtx({})
|
|
attachBareOsTuiSdk(ctx)
|
|
const d = ctx.tui.createDecoder()
|
|
d.push('\x1b')
|
|
t.is(d.take().length, 0)
|
|
const esc = d.flushEscape()
|
|
t.ok(ctx.tui.key.matches(esc, 'escape'))
|
|
})
|
|
|
|
test('withSession acquire/release and restore on throw', async (t) => {
|
|
const ctx = makeCtx({})
|
|
attachBareOsTuiSdk(ctx)
|
|
const c = ctx._counts()
|
|
let threw = false
|
|
try {
|
|
await ctx.tui.withSession(() => {
|
|
t.ok(c.stdin.raw)
|
|
t.ok(c.writes.join('').indexOf('1049h') >= 0)
|
|
throw new Error('boom')
|
|
})
|
|
} catch (e) {
|
|
threw = e.message === 'boom'
|
|
}
|
|
t.ok(threw)
|
|
t.is(c.suspends, 1)
|
|
t.is(c.resumes, 1)
|
|
t.absent(c.stdin.raw)
|
|
const out = c.writes.join('')
|
|
t.ok(out.indexOf('1049l') >= 0)
|
|
t.ok(out.indexOf('?25h') >= 0)
|
|
})
|
|
|
|
test('nested acquire is refcounted', (t) => {
|
|
const ctx = makeCtx({})
|
|
attachBareOsTuiSdk(ctx)
|
|
const c = ctx._counts()
|
|
ctx.tui.acquire()
|
|
ctx.tui.acquire()
|
|
t.is(c.suspends, 1)
|
|
t.ok(c.stdin.raw)
|
|
ctx.tui.release()
|
|
t.is(c.resumes, 0)
|
|
t.ok(c.stdin.raw)
|
|
ctx.tui.release()
|
|
t.is(c.resumes, 1)
|
|
t.absent(c.stdin.raw)
|
|
})
|
|
|
|
test('captured stdout is not a TTY', (t) => {
|
|
const ctx = makeCtx({})
|
|
ctx.bareOsStdoutCaptured = true
|
|
attachBareOsTuiSdk(ctx)
|
|
t.absent(ctx.tui.isTTY())
|
|
})
|
|
|
|
test('style width is ANSI- and wide-glyph aware', (t) => {
|
|
const ctx = makeCtx({})
|
|
attachBareOsTuiSdk(ctx)
|
|
t.is(ctx.tui.style.width('hi'), 2)
|
|
t.is(ctx.tui.style.width('\x1b[32mhi\x1b[0m'), 2)
|
|
t.is(ctx.tui.style.width('中'), 2)
|
|
t.is(ctx.tui.style.stripAnsi('\x1b[32mhi\x1b[0m'), 'hi')
|
|
t.is(ctx.tui.style.height('a\nb\nc'), 3)
|
|
const boxed = ctx.tui
|
|
.style()
|
|
.border(ctx.tui.style.borders.rounded)
|
|
.render('hi')
|
|
t.ok(boxed.indexOf('hi') >= 0)
|
|
t.ok(ctx.tui.style.height(boxed) >= 3)
|
|
const joined = ctx.tui.style.joinHorizontal(
|
|
ctx.tui.style.position.top,
|
|
'A',
|
|
'B'
|
|
)
|
|
t.is(joined, 'AB')
|
|
})
|
|
|
|
test('NO_COLOR skips SGR in style.render', (t) => {
|
|
const ctx = makeCtx({ NO_COLOR: '1' })
|
|
attachBareOsTuiSdk(ctx)
|
|
const painted = ctx.tui.style().bold(true).foreground('cyan').render('hi')
|
|
t.is(painted, 'hi')
|
|
t.ok(ctx.tui.theme().noColor)
|
|
})
|
|
|
|
test('theme name follows BARE_OS_THEME', (t) => {
|
|
const ctx = makeCtx({ BARE_OS_THEME: 'nord' })
|
|
attachBareOsTuiSdk(ctx)
|
|
t.is(ctx.tui.theme().name, 'nord')
|
|
t.is(ctx.sdk.theme.name(), 'nord')
|
|
t.ok(ctx.tui.theme().tokens.accent)
|
|
})
|
|
|
|
test('Program counter: up increments then quit', async (t) => {
|
|
const ctx = makeCtx({})
|
|
attachBareOsTuiSdk(ctx)
|
|
const app = {
|
|
n: 0,
|
|
init() {
|
|
return null
|
|
},
|
|
update(msg) {
|
|
if (ctx.tui.key.matches(msg, 'up', 'k')) this.n++
|
|
if (ctx.tui.key.matches(msg, 'q', 'ctrl+c')) return [this, ctx.tui.quit]
|
|
return [this, null]
|
|
},
|
|
view() {
|
|
return 'count: ' + this.n
|
|
}
|
|
}
|
|
const p = ctx.tui.create(app, {
|
|
input: ctx.replStdin,
|
|
output: ctx.replStdout,
|
|
isTTY: true,
|
|
fps: 0,
|
|
width: 40,
|
|
height: 8
|
|
})
|
|
const up = ctx.tui.decode('\x1b[A')[0]
|
|
p.send(up)
|
|
p.send(up)
|
|
p.quit()
|
|
await p.run()
|
|
t.is(app.n, 2)
|
|
t.ok(ctx._counts().writes.join('').indexOf('count: 2') >= 0)
|
|
t.ok(ctx._counts().writes.join('').indexOf('1049l') >= 0)
|
|
t.is(ctx.sdk.tui, ctx.tui)
|
|
})
|
|
|
|
test('Program restores terminal when update throws', async (t) => {
|
|
const ctx = makeCtx({})
|
|
attachBareOsTuiSdk(ctx)
|
|
const app = {
|
|
update() {
|
|
throw new Error('bad update')
|
|
},
|
|
view() {
|
|
return 'x'
|
|
}
|
|
}
|
|
let err = null
|
|
try {
|
|
await ctx.tui.run(app, {
|
|
input: ctx.replStdin,
|
|
output: ctx.replStdout,
|
|
isTTY: true,
|
|
fps: 0
|
|
})
|
|
} catch (e) {
|
|
err = e
|
|
}
|
|
t.ok(err && err.message === 'bad update')
|
|
const out = ctx._counts().writes.join('')
|
|
t.ok(out.indexOf('1049l') >= 0)
|
|
t.ok(out.indexOf('?25h') >= 0)
|
|
t.absent(ctx._counts().stdin.raw)
|
|
})
|
|
|
|
test('Program refuses a non-TTY without allowDumb', async (t) => {
|
|
const ctx = makeCtx({}, { tty: false })
|
|
attachBareOsTuiSdk(ctx)
|
|
const app = {
|
|
update() {
|
|
return [this, ctx.tui.quit]
|
|
},
|
|
view() {
|
|
return 'x'
|
|
}
|
|
}
|
|
await ctx.tui.run(app, { fps: 0 })
|
|
t.is(ctx.exitCode, 1)
|
|
})
|
|
|
|
test('decoder names space as space', (t) => {
|
|
const ctx = makeCtx({})
|
|
attachBareOsTuiSdk(ctx)
|
|
const ev = ctx.tui.decode(' ')[0]
|
|
t.ok(ctx.tui.key.matches(ev, 'space'))
|
|
})
|
|
|
|
test('widgets: checkbox toggle and list select', (t) => {
|
|
const ctx = makeCtx({})
|
|
attachBareOsTuiSdk(ctx)
|
|
const box = ctx.tui.checkbox.create({ label: 'ok' }).focus()
|
|
const space = ctx.tui.decode(' ')[0]
|
|
box.update(space)
|
|
t.ok(box.checked)
|
|
t.ok(box.view().indexOf('[x]') >= 0)
|
|
|
|
const list = ctx.tui.list.create({
|
|
items: ['alpha', 'beta', 'gamma'],
|
|
height: 3,
|
|
filterable: false
|
|
})
|
|
const down = ctx.tui.decode('\x1b[B')[0]
|
|
list.update(down)
|
|
t.is(list.selectedItem(), 'beta')
|
|
t.ok(list.view().indexOf('beta') >= 0)
|
|
})
|
|
|
|
test('widgets: textinput insert and progress view', (t) => {
|
|
const ctx = makeCtx({})
|
|
attachBareOsTuiSdk(ctx)
|
|
const field = ctx.tui.textinput.create({ focused: true })
|
|
field.update(ctx.tui.decode('a')[0])
|
|
field.update(ctx.tui.decode('b')[0])
|
|
t.is(field.value, 'ab')
|
|
const bar = ctx.tui.progress.create({ width: 12 })
|
|
const v = bar.view(0.5)
|
|
t.ok(v.indexOf('50%') >= 0)
|
|
})
|
|
|
|
test('widgets: focus ring tabs between fields', (t) => {
|
|
const ctx = makeCtx({})
|
|
attachBareOsTuiSdk(ctx)
|
|
const a = ctx.tui.textinput.create({ prompt: 'A' })
|
|
const b = ctx.tui.checkbox.create({ label: 'B' })
|
|
const ring = ctx.tui.focus.create({ items: [a, b] })
|
|
t.ok(ring.focused().focused)
|
|
const tab = ctx.tui.decode('\t')[0]
|
|
ring.update(tab)
|
|
t.ok(ring.items[1].focused)
|
|
t.absent(ring.items[0].focused)
|
|
})
|
|
|
|
test('tick command fires a message then quit', async (t) => {
|
|
const ctx = makeCtx({})
|
|
attachBareOsTuiSdk(ctx)
|
|
const app = {
|
|
ticks: 0,
|
|
init() {
|
|
return ctx.tui.tick(1, function () {
|
|
return { type: 'tock' }
|
|
})
|
|
},
|
|
update(msg) {
|
|
if (msg && msg.type === 'tock') {
|
|
this.ticks++
|
|
return [this, ctx.tui.quit]
|
|
}
|
|
return [this, null]
|
|
},
|
|
view() {
|
|
return 't:' + this.ticks
|
|
}
|
|
}
|
|
await ctx.tui.run(app, {
|
|
input: ctx.replStdin,
|
|
output: ctx.replStdout,
|
|
isTTY: true,
|
|
fps: 0
|
|
})
|
|
t.is(app.ticks, 1)
|
|
})
|
|
|
|
test('filepicker mock lists and selects a file', async (t) => {
|
|
const ctx = makeCtx({})
|
|
attachBareOsTuiSdk(ctx)
|
|
const m = ctx.tui.filepicker.mock({
|
|
docs: { 'a.md': 1 },
|
|
'r.txt': 1
|
|
})
|
|
const fp = ctx.tui.filepicker.create({ vfs: m.vfs, cwd: '/', height: 6 })
|
|
const listed = await fp.init()()
|
|
fp.update(listed)
|
|
t.ok(fp.entries.length >= 2)
|
|
fp.update(ctx.tui.decode('\x1b[B')[0])
|
|
const pair = fp.update(ctx.tui.decode('\r')[0])
|
|
t.ok(typeof pair[1] === 'function')
|
|
const sel = await pair[1]()
|
|
t.is(sel.type, 'filepicker.select')
|
|
t.is(sel.path, '/r.txt')
|
|
})
|
|
|
|
test('confirm model accepts y', async (t) => {
|
|
const ctx = makeCtx({})
|
|
attachBareOsTuiSdk(ctx)
|
|
const app = ctx.tui.confirm.create({ title: 'Delete?' })
|
|
const p = ctx.tui.create(app, {
|
|
input: ctx.replStdin,
|
|
output: ctx.replStdout,
|
|
isTTY: true,
|
|
fps: 0
|
|
})
|
|
p.send(ctx.tui.decode('y')[0])
|
|
await p.run()
|
|
t.ok(app.result)
|
|
})
|
|
|
|
test('prompt model collects text', async (t) => {
|
|
const ctx = makeCtx({})
|
|
attachBareOsTuiSdk(ctx)
|
|
const app = ctx.tui.prompt.create({ label: 'Name: ' })
|
|
const p = ctx.tui.create(app, {
|
|
input: ctx.replStdin,
|
|
output: ctx.replStdout,
|
|
isTTY: true,
|
|
fps: 0
|
|
})
|
|
p.send(ctx.tui.decode('a')[0])
|
|
p.send(ctx.tui.decode('\r')[0])
|
|
await p.run()
|
|
t.is(app.result, 'a')
|
|
})
|
|
|
|
test('form required + ctrl+s submits values', (t) => {
|
|
const ctx = makeCtx({})
|
|
attachBareOsTuiSdk(ctx)
|
|
const f = ctx.tui.form.create({
|
|
title: 'Account',
|
|
fields: [
|
|
ctx.tui.form.text({ name: 'user', label: 'User', required: true }),
|
|
ctx.tui.form.confirm({ name: 'tos', label: 'TOS', required: true })
|
|
]
|
|
})
|
|
const fail = f.update(ctx.tui.decode('\x13')[0])
|
|
t.absent(fail[1])
|
|
t.ok(f.errors().user)
|
|
f.update(ctx.tui.decode('z')[0])
|
|
f.update(ctx.tui.decode('\t')[0])
|
|
f.update(ctx.tui.decode(' ')[0])
|
|
const ok = f.update(ctx.tui.decode('\x13')[0])
|
|
t.ok(typeof ok[1] === 'function')
|
|
const msg = ok[1]()
|
|
t.is(msg.type, 'form.submit')
|
|
t.is(msg.values.user, 'z')
|
|
t.ok(msg.values.tos)
|
|
})
|
|
|
|
test('choose model picks an option', async (t) => {
|
|
const ctx = makeCtx({})
|
|
attachBareOsTuiSdk(ctx)
|
|
const app = ctx.tui.choose.create({
|
|
title: 'Plan',
|
|
options: ['free', 'pro']
|
|
})
|
|
const p = ctx.tui.create(app, {
|
|
input: ctx.replStdin,
|
|
output: ctx.replStdout,
|
|
isTTY: true,
|
|
fps: 0
|
|
})
|
|
p.send(ctx.tui.decode('\x1b[B')[0])
|
|
p.send(ctx.tui.decode('\r')[0])
|
|
await p.run()
|
|
t.is(app.result, 'pro')
|
|
})
|
|
|
|
test('markdown subset renders headings and lists', (t) => {
|
|
const ctx = makeCtx({})
|
|
attachBareOsTuiSdk(ctx)
|
|
const html = ctx.tui.markdown.render('# Title\n- item\n**bold**')
|
|
t.ok(html.indexOf('Title') >= 0)
|
|
t.ok(html.indexOf('• ') >= 0)
|
|
t.ok(html.indexOf('bold') >= 0)
|
|
})
|
|
|
|
test('tabs switch with arrows', (t) => {
|
|
const ctx = makeCtx({})
|
|
attachBareOsTuiSdk(ctx)
|
|
const tabs = ctx.tui.tabs.create({
|
|
tabs: [
|
|
{ title: 'A', body: 'one' },
|
|
{ title: 'B', body: 'two' }
|
|
]
|
|
})
|
|
t.ok(tabs.view().indexOf('one') >= 0)
|
|
tabs.update(ctx.tui.decode('\x1b[C')[0])
|
|
t.is(tabs.index, 1)
|
|
t.ok(tabs.view().indexOf('two') >= 0)
|
|
})
|
|
|
|
test('sdk env / ipc / proc helpers', async (t) => {
|
|
const ctx = makeCtx({ TERM: 'xterm-256color', NO_COLOR: '' })
|
|
ctx.env.HOME = '/home/guest'
|
|
ctx.vfs = {
|
|
async readdir() {
|
|
return ['a']
|
|
},
|
|
async readFile(p) {
|
|
if (String(p).indexOf('version') >= 0) return new Uint8Array([49])
|
|
return null
|
|
}
|
|
}
|
|
ctx.b4a = {
|
|
toString(buf) {
|
|
return Buffer.from(buf).toString()
|
|
},
|
|
from(s) {
|
|
return Buffer.from(String(s))
|
|
}
|
|
}
|
|
let pushed = null
|
|
ctx.bareOsIpc = {
|
|
pushJson(name, obj) {
|
|
pushed = { name, obj }
|
|
},
|
|
async takeJson() {
|
|
return pushed && pushed.obj
|
|
}
|
|
}
|
|
attachBareOsTuiSdk(ctx)
|
|
t.is(ctx.sdk.env.get('TERM'), 'xterm-256color')
|
|
t.is(ctx.sdk.env.term(), 'xterm-256color')
|
|
t.ok(ctx.sdk.env.colorDepth())
|
|
t.ok(ctx.sdk.theme.name())
|
|
t.ok(ctx.sdk.ipc.pushJson('ch', { a: 1 }))
|
|
t.is((await ctx.sdk.ipc.takeJson('ch')).a, 1)
|
|
t.is(await ctx.sdk.proc.read('version'), '1')
|
|
t.alike(await ctx.sdk.vfs.list('/'), ['a'])
|
|
})
|
|
|
|
test('cell buffer blit does not change row count', (t) => {
|
|
const ctx = makeCtx({})
|
|
attachBareOsTuiSdk(ctx)
|
|
const grid = ctx.tui.buffer.create(5, 20)
|
|
ctx.tui.buffer.fill(grid, 'one\ntwo\nthree\nfour\nfive')
|
|
t.is(ctx.tui.buffer.plain(grid).length, 5)
|
|
ctx.tui.buffer.blit(grid, 1, 2, 'POP')
|
|
const lines = ctx.tui.buffer.plain(grid)
|
|
t.is(lines.length, 5)
|
|
t.ok(lines[1].indexOf('POP') >= 0)
|
|
t.is(lines[0], 'one')
|
|
t.is(lines[4], 'five')
|
|
})
|
|
|
|
test('Program cell buffer paints overlay without extra lines', async (t) => {
|
|
const ctx = makeCtx({})
|
|
attachBareOsTuiSdk(ctx)
|
|
const app = {
|
|
open: true,
|
|
init() {
|
|
return null
|
|
},
|
|
update(msg) {
|
|
if (ctx.tui.key.matches(msg, 'q')) return [this, ctx.tui.quit]
|
|
return [this, null]
|
|
},
|
|
view() {
|
|
return 'AAAA\nBBBB\nCCCC\nDDDD'
|
|
},
|
|
overlay() {
|
|
if (!this.open) return null
|
|
return { row: 1, col: 1, text: 'XY' }
|
|
}
|
|
}
|
|
const p = ctx.tui.create(app, {
|
|
input: ctx.replStdin,
|
|
output: ctx.replStdout,
|
|
isTTY: true,
|
|
fps: 0,
|
|
buffer: 'cell',
|
|
width: 8,
|
|
height: 4
|
|
})
|
|
p.quit()
|
|
await p.run()
|
|
t.ok(p.renderer.plain()[1].indexOf('XY') >= 0)
|
|
t.is(p.renderer.plain().length, 4)
|
|
})
|
|
|
|
test('modal overlay() is centered and view stays child', (t) => {
|
|
const ctx = makeCtx({})
|
|
attachBareOsTuiSdk(ctx)
|
|
const child = {
|
|
view() {
|
|
return 'body-line-1\nbody-line-2\nbody-line-3'
|
|
}
|
|
}
|
|
const modal = ctx.tui.modal.create({ child: child, open: true, title: 'Ask' })
|
|
t.is(modal.view(), child.view())
|
|
const ov = modal.overlay({ width: 40, height: 12 })
|
|
t.ok(ov)
|
|
t.ok(ov.row >= 0)
|
|
t.ok(ov.text.indexOf('Ask') >= 0)
|
|
})
|
|
|
|
test('tree expands children', (t) => {
|
|
const ctx = makeCtx({})
|
|
attachBareOsTuiSdk(ctx)
|
|
const tree = ctx.tui.tree.create({
|
|
items: [{ title: 'root', children: [{ title: 'leaf' }], open: false }]
|
|
})
|
|
t.absent(tree.view().indexOf('leaf') >= 0)
|
|
tree.update(ctx.tui.decode('\x1b[C')[0])
|
|
t.ok(tree.view().indexOf('leaf') >= 0)
|
|
})
|