OverviewFilesCommitsIssuesDocsReleasesPeers

codetree / test / render.test.js

107 lines · javascript ref:
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')
})