OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / dashboard-docs-ui.test.js

195 lines · javascript ref:
import test from 'brittle'
import { readFileSync } from 'node:fs'
import vm from 'node:vm'

const docs = readFileSync('dashboard/docs.html', 'utf8')

test('docs copy buttons avoid inline handlers and protect external tabs', (t) => {
  const copyButtons = docs.match(/<button\b[^>]*class="copy-btn"[^>]*>/g) || []
  const blankLinks = docs.match(/<a\b[^>]*target="_blank"[^>]*>/g) || []

  t.ok(copyButtons.length > 20, 'docs page has many copyable code blocks')
  t.ok(copyButtons.every(tag => /\btype="button"/.test(tag)), 'copy controls are explicit buttons')
  t.ok(copyButtons.every(tag => /\bdata-docs-action="copy-code"/.test(tag)), 'copy controls use delegated action hooks')
  t.ok(blankLinks.length >= 2, 'external docs links still open in new tabs')
  t.ok(blankLinks.every(tag => /\brel="noopener noreferrer"/.test(tag)), 'new-tab links prevent opener access')
  t.absent(docs.includes('onclick="copyCode'))
  t.absent(docs.includes('onclick='))
})

test('docs copy handler writes the adjacent code block', async (t) => {
  const state = { written: null, timeoutMs: null }
  const btn = {
    textContent: 'Copy',
    classList: {
      added: [],
      removed: [],
      add (name) { this.added.push(name) },
      remove (name) { this.removed.push(name) }
    },
    closest (selector) {
      t.is(selector, '.code-wrap')
      return {
        querySelector (innerSelector) {
          t.is(innerSelector, 'pre')
          return { textContent: 'p2p-hiverelay start --region NA --port 9100' }
        }
      }
    }
  }

  const source = [
    extractFunction('getCodeText'),
    extractFunction('writeClipboard'),
    extractFunction('markCopied'),
    extractFunction('copyCode'),
    'copyCode(btn)'
  ].join('\n')

  await vm.runInNewContext(source, {
    btn,
    navigator: {
      clipboard: {
        async writeText (text) {
          state.written = text
        }
      }
    },
    setTimeout (_fn, delay) {
      state.timeoutMs = delay
    }
  })

  t.is(state.written, 'p2p-hiverelay start --region NA --port 9100')
  t.is(btn.textContent, 'Copied!')
  t.alike(btn.classList.added, ['copied'])
  t.is(state.timeoutMs, 2000)
})

test('docs copy handler uses legacy fallback and reports fallback failures', async (t) => {
  const source = [
    extractFunction('getCodeText'),
    extractFunction('writeClipboard'),
    extractFunction('markCopied'),
    extractFunction('markCopyFailed'),
    extractFunction('copyCode'),
    `
    (async function () {
      function makeBtn () {
        return {
          textContent: 'Copy',
          classList: {
            added: [],
            removed: [],
            add: function (name) { this.added.push(name) },
            remove: function (name) { this.removed.push(name) }
          },
          closest: function () {
            return { querySelector: function () { return { textContent: 'hiverelay docs copy' } } }
          }
        }
      }
      function makeTextarea () {
        return {
          value: '',
          style: {},
          selected: false,
          removed: false,
          setAttribute: function (name, value) { this[name] = value },
          select: function () { this.selected = true },
          remove: function () { this.removed = true }
        }
      }

      var timers = []
      globalThis.setTimeout = function (fn, delay) { timers.push(delay); return timers.length }
      delete globalThis.navigator
      var textareas = []
      globalThis.document = {
        body: {
          appended: [],
          appendChild: function (el) { this.appended.push(el); return el }
        },
        createElement: function () {
          var textarea = makeTextarea()
          textareas.push(textarea)
          return textarea
        },
        execCommand: function (cmd) {
          this.command = cmd
          return true
        }
      }

      var okBtn = makeBtn()
      await copyCode(okBtn)

      var failBtn = makeBtn()
      globalThis.document.execCommand = function () { return false }
      var failCode = null
      try {
        await copyCode(failBtn)
      } catch (err) {
        failCode = err.code
        markCopyFailed(failBtn, err.code === 'clipboard-unavailable' ? 'Clipboard unavailable' : 'Copy failed')
      }

      var unavailableBtn = makeBtn()
      delete globalThis.document.execCommand
      var unavailableCode = null
      try {
        await copyCode(unavailableBtn)
      } catch (err) {
        unavailableCode = err.code
        markCopyFailed(unavailableBtn, err.code === 'clipboard-unavailable' ? 'Clipboard unavailable' : 'Copy failed')
      }

      return {
        okText: okBtn.textContent,
        okClass: okBtn.classList.added.join(','),
        textareaValue: textareas[0].value,
        selected: textareas[0].selected,
        removed: textareas[0].removed,
        command: globalThis.document.command,
        failCode: failCode,
        failText: failBtn.textContent,
        unavailableCode: unavailableCode,
        unavailableText: unavailableBtn.textContent,
        timers: timers.join(',')
      }
    })()
    `
  ].join('\n')

  const out = await vm.runInNewContext(source, { Promise, Error })

  t.is(out.okText, 'Copied!')
  t.is(out.okClass, 'copied')
  t.is(out.textareaValue, 'hiverelay docs copy')
  t.is(out.selected, true)
  t.is(out.removed, true)
  t.is(out.command, 'copy')
  t.is(out.failCode, 'copy-failed')
  t.is(out.failText, 'Copy failed')
  t.is(out.unavailableCode, 'clipboard-unavailable')
  t.is(out.unavailableText, 'Clipboard unavailable')
  t.is(out.timers, '2000,2000,2000')
})

function extractFunction (name) {
  let start = docs.indexOf('function ' + name)
  if (start === -1) throw new Error('missing function ' + name)
  if (docs.slice(start - 6, start) === 'async ') start -= 6
  const firstBrace = docs.indexOf('{', start)
  let depth = 0
  for (let i = firstBrace; i < docs.length; i++) {
    const char = docs[i]
    if (char === '{') depth++
    else if (char === '}') {
      depth--
      if (depth === 0) return docs.slice(start, i + 1)
    }
  }
  throw new Error('unterminated function ' + name)
}