import { test } from 'brittle'
import { readFileSync } from 'fs'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
import { scan } from '../lib/scanner.js'
import { renderPage } from '../lib/render.js'
import { parse as acornParse } from '../vendor/acorn/dist/acorn.mjs'
const HERE = dirname(fileURLToPath(import.meta.url))
const FIXTURE = resolve(HERE, 'fixtures/connectors')
const BACKTICK = String.fromCharCode(96)
// render.js emits a whole browser app as a template literal. Nothing type-checks
// it, lints it, or parses it at build time — a stray brace ships a white page.
// These tests are the parse gate.
function inlineScript (html) {
const open = html.indexOf('<script>')
const close = html.lastIndexOf('</script>')
if (open === -1 || close === -1) throw new Error('no inline script found')
return html.slice(open + '<script>'.length, close)
}
test('render: the emitted page parses as JavaScript', async (t) => {
const tree = await scan(FIXTURE)
const html = renderPage(tree, '')
const js = inlineScript(html)
t.ok(js.length > 1000, 'got a substantial script')
try {
acornParse(js, { ecmaVersion: 'latest', sourceType: 'script', locations: true })
t.pass('inline script parses')
} catch (err) {
t.fail('inline script is not valid JS: ' + err.message)
}
})
test('render: the page literal contains NO backtick', (t) => {
// renderPage returns one enormous template literal, so a single backtick
// anywhere inside it — including inside a comment — terminates the literal and
// breaks the module. That happened twice in one sitting while editing
// comments, and it is invisible until the file is imported.
const src = readFileSync(resolve(HERE, '../lib/render.js'), 'utf-8')
const start = src.indexOf('return ' + BACKTICK + '<!DOCTYPE html>')
t.ok(start > 0, 'found the page literal')
const body = src.slice(start + ('return ' + BACKTICK).length, src.lastIndexOf(BACKTICK))
const lines = body.split('\n')
const offenders = []
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes(BACKTICK)) offenders.push((i + 1) + ': ' + lines[i].trim().slice(0, 60))
}
t.alike(offenders, [], 'no backticks inside the page literal: ' + offenders.join(' | '))
})
test('render: no duplicate top-level function declarations', async (t) => {
const tree = await scan(FIXTURE)
const js = inlineScript(renderPage(tree, ''))
const ast = acornParse(js, { ecmaVersion: 'latest', sourceType: 'script' })
const seen = new Map()
const dupes = []
for (const node of ast.body) {
if (node.type !== 'FunctionDeclaration' || !node.id) continue
if (seen.has(node.id.name)) dupes.push(node.id.name)
seen.set(node.id.name, true)
}
// A second declaration silently replaces the first for the whole script.
// escapeHtml was declared twice, and the weaker one won.
t.alike(dupes, [], 'no function name is declared twice: ' + dupes.join(', '))
})
test('render: embedded tree JSON cannot break out of the script tag', async (t) => {
const tree = await scan(FIXTURE)
// Smuggle a closing tag through data that reaches the inlined JSON.
tree.modules['connectors-fixture'].files['index.js'].role = '</script><b>x'
const html = renderPage(tree, '')
const open = html.indexOf('<script>')
const firstClose = html.indexOf('</script>', open)
const js = html.slice(open + '<script>'.length, firstClose)
try {
acornParse(js, { ecmaVersion: 'latest', sourceType: 'script' })
t.pass('script survives a </script> in the data')
} catch (err) {
t.fail('data containing </script> truncated the script: ' + err.message)
}
})
test('render: the node view keys do not fire while you are typing', (t) => {
// The page has a search box and a path field. Without a guard on the event
// target, every single-letter shortcut fires mid-word — typing "graph" into
// search would gather, toggle frames and toggle routing on the way past.
const src = readFileSync(resolve(HERE, '../lib/render-nodes.js'), 'utf-8')
const handler = src.slice(src.indexOf("addEventListener('keydown'"))
const body = handler.slice(0, handler.indexOf('\n })'))
t.ok(body.includes('tagName'), 'the handler inspects the event target')
t.ok(/INPUT/.test(body) && /TEXTAREA/.test(body), 'and bails out for text fields')
t.ok(body.includes('isContentEditable'), 'contentEditable too')
// And it must not act on another tab's keystrokes.
t.ok(body.includes('currentTab'), 'the node view only takes keys while it is up')
})