/**
* 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')
})