OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / test / unit / namespace-ui.test.js

213 lines · javascript ref:
/**
 * Unit tests for lib/namespace-ui.js
 *
 * Tests that the rendered HTML includes expected elements.
 */

import test from 'brittle'
import { renderGroupsPage, renderGroupPage } from '../../lib/namespace-ui.js'

// ========================================
// Groups List Page
// ========================================

test('renderGroupsPage - includes join group button', (t) => {
  const html = renderGroupsPage([], '')

  t.ok(html.includes('Join Group'), 'has Join Group button text')
  t.ok(html.includes('showJoinModal()'), 'button calls showJoinModal')
})

test('renderGroupsPage - includes join modal', (t) => {
  const html = renderGroupsPage([], '')

  t.ok(html.includes('id="join-modal"'), 'has join modal element')
  t.ok(html.includes('id="join-step-invite"'), 'has step 1: invite input')
  t.ok(html.includes('id="join-step-preview"'), 'has step 2: repo preview')
  t.ok(html.includes('id="join-step-success"'), 'has step 3: success')
  t.ok(html.includes('id="join-invite-hex"'), 'has invite hex textarea')
  t.ok(html.includes('connectWithInvite()'), 'has connect function call')
  t.ok(html.includes('confirmJoin()'), 'has confirm function call')
})

test('renderGroupsPage - includes create group form for admins', (t) => {
  const html = renderGroupsPage([], '')

  t.ok(html.includes('id="create-form"'), 'has create form')
  t.ok(html.includes('+ New Group'), 'has new group button')
})

test('renderGroupsPage - renders group cards', (t) => {
  const groups = [
    { name: 'test-group', displayName: 'Test Group', description: 'A test', repos: ['repo1'], members: [{ userId: 'u1', role: 'admin' }] },
    { name: 'another', displayName: 'Another', description: '', repos: [], members: [] }
  ]

  const html = renderGroupsPage(groups, '')

  t.ok(html.includes('Test Group'), 'renders group display name')
  t.ok(html.includes('A test'), 'renders description')
  t.ok(html.includes('1 repo'), 'renders repo count')
  t.ok(html.includes('1 member'), 'renders member count')
  t.ok(html.includes('/groups/test-group'), 'links to group detail')
  t.ok(html.includes('Another'), 'renders second group')
})

test('renderGroupsPage - shows empty state when no groups', (t) => {
  const html = renderGroupsPage([], '')

  t.ok(html.includes('No groups yet'), 'shows empty state')
})

test('renderGroupsPage - respects pathPrefix', (t) => {
  const html = renderGroupsPage([], '/git')

  t.ok(html.includes('/git/groups'), 'prefixes group links')
  t.ok(html.includes('/git/'), 'prefixes nav links')
})

test('renderGroupsPage - join modal posts to correct API', (t) => {
  const html = renderGroupsPage([], '/prefix')

  t.ok(html.includes("getBasePath() + '/api/groups/join'"), 'join API path uses getBasePath')
})

// ========================================
// Status Banner
// ========================================

test('renderGroupsPage - shows local mode banner by default', (t) => {
  const html = renderGroupsPage([], '')

  t.ok(html.includes('status-banner'), 'has status banner')
  t.ok(html.includes('Local mode'), 'shows local mode text')
  t.ok(html.includes('status-dot local'), 'has gray local dot')
})

test('renderGroupsPage - shows shared banner when registry active', (t) => {
  const status = { active: true, mode: 'shared', writerCount: 2 }
  const html = renderGroupsPage([], '', status)

  t.ok(html.includes('status-banner'), 'has status banner')
  t.ok(html.includes('2 servers linked'), 'shows server count')
  t.ok(html.includes('status-dot shared'), 'has green shared dot')
  t.absent(html.includes('Local mode'), 'does not show local mode')
})

test('renderGroupsPage - shows 1 server linked (singular)', (t) => {
  const status = { active: true, mode: 'shared', writerCount: 1 }
  const html = renderGroupsPage([], '', status)

  t.ok(html.includes('1 server linked'), 'singular form')
  t.absent(html.includes('1 servers'), 'not plural')
})

// ========================================
// Unlink Button
// ========================================

test('renderGroupsPage - shows unlink button when registry active', (t) => {
  const status = { active: true, mode: 'shared', writerCount: 2 }
  const html = renderGroupsPage([], '', status)

  t.ok(html.includes('unlinkRegistry()'), 'has unlink function call')
  t.ok(html.includes('Unlink'), 'has Unlink button text')
  t.ok(html.includes('admin-ctrl'), 'unlink button is admin-only')
})

test('renderGroupsPage - no unlink button in local mode', (t) => {
  const html = renderGroupsPage([], '')

  // The onclick="unlinkRegistry()" only appears inside the status banner when shared
  // In local mode, the banner has no Unlink button (though the function def exists in script)
  t.absent(html.includes('onclick="unlinkRegistry()"'), 'no unlink onclick in local mode banner')
})

// ========================================
// Source Badges
// ========================================

test('renderGroupsPage - shows source badges when mixed sources', (t) => {
  const groups = [
    { name: 'local-g', displayName: 'Local', description: '', repos: [], members: [], source: 'local' },
    { name: 'shared-g', displayName: 'Shared', description: '', repos: [], members: [], source: 'shared' }
  ]
  const status = { active: true, mode: 'shared', writerCount: 2 }
  const html = renderGroupsPage(groups, '', status)

  t.ok(html.includes('source-badge local'), 'has local source badge')
  t.ok(html.includes('source-badge shared'), 'has shared source badge')
})

test('renderGroupsPage - hides source badges when all same source', (t) => {
  const groups = [
    { name: 'a', displayName: 'A', description: '', repos: [], members: [], source: 'local' },
    { name: 'b', displayName: 'B', description: '', repos: [], members: [], source: 'local' }
  ]
  const html = renderGroupsPage(groups, '')

  // source-badge appears in CSS, so check for actual badge instances with content
  t.absent(html.includes('source-badge local">local'), 'no local source badge instances')
  t.absent(html.includes('source-badge shared">shared'), 'no shared source badge instances')
})

// ========================================
// Group Detail Page
// ========================================

test('renderGroupPage - includes invite server button', (t) => {
  const group = { name: 'mygroup', displayName: 'My Group', type: 'group', repos: [], members: [], description: '' }
  const html = renderGroupPage(group, [], [], '')

  t.ok(html.includes('Invite Server'), 'has invite server button')
  t.ok(html.includes('createServerInvite()'), 'button calls createServerInvite')
})

test('renderGroupPage - invite panel references /groups not /join', (t) => {
  const group = { name: 'mygroup', displayName: 'My Group', type: 'group', repos: [], members: [], description: '' }
  const html = renderGroupPage(group, [], [], '')

  t.ok(html.includes('/groups'), 'references /groups page')
  t.absent(html.includes('href="/join"'), 'does not link to standalone /join')
})

// ========================================
// Linked Servers
// ========================================

test('renderGroupPage - shows linked servers for shared groups', (t) => {
  const group = { name: 'shared-group', displayName: 'Shared', type: 'group', repos: [], members: [], description: '', source: 'shared' }
  const serverInfo = {
    selfKey: 'abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234',
    writers: [
      { serverKey: 'abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234', role: 'owner' },
      { serverKey: 'efef5678efef5678efef5678efef5678efef5678efef5678efef5678efef5678', role: 'writer' }
    ]
  }
  const html = renderGroupPage(group, [], [], '', serverInfo)

  t.ok(html.includes('Federation'), 'has federation heading')
  t.ok(html.includes('2 servers sharing'), 'shows server count')
  t.ok(html.includes('abcd1234abcd1234'), 'shows server key prefix')
  t.ok(html.includes('This server'), 'marks self server')
  t.ok(html.includes('efef5678efef5678'), 'shows second server key')
})

test('renderGroupPage - hides linked servers for local groups', (t) => {
  const group = { name: 'local-group', displayName: 'Local', type: 'group', repos: [], members: [], description: '', source: 'local' }
  const serverInfo = {
    selfKey: 'aaaa',
    writers: [{ serverKey: 'aaaa', role: 'owner' }]
  }
  const html = renderGroupPage(group, [], [], '', serverInfo)

  t.absent(html.includes('Linked Servers'), 'no linked servers for local group')
})

test('renderGroupPage - no linked servers section without serverInfo', (t) => {
  const group = { name: 'mygroup', displayName: 'My Group', type: 'group', repos: [], members: [], description: '' }
  const html = renderGroupPage(group, [], [], '', null)

  t.absent(html.includes('Linked Servers'), 'no linked servers when no serverInfo')
})