frontend: polish app launch and release experience
This commit is contained in:
@@ -67,6 +67,13 @@
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-2">
|
||||
<div v-if="loading && nodes.length > 0" class="p-2 text-center text-white/45 text-xs flex items-center justify-center gap-2">
|
||||
<svg class="animate-spin h-3.5 w-3.5" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Searching relays...
|
||||
</div>
|
||||
<div
|
||||
v-for="node in nodes"
|
||||
:key="node.nostr_pubkey"
|
||||
@@ -189,4 +196,6 @@ function shortNpub(npub: string): string {
|
||||
if (!npub || npub.length < 16) return npub
|
||||
return `${npub.slice(0, 14)}…${npub.slice(-8)}`
|
||||
}
|
||||
|
||||
defineExpose({ refresh })
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-if="node" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/10 backdrop-blur-md" @click.self="handleClose">
|
||||
<div v-if="node" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-md" @click.self="handleClose">
|
||||
<div class="glass-card p-6 w-full max-w-lg max-h-[80vh] overflow-y-auto">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-semibold text-white">Node Details</h2>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<div class="glass-card p-6 max-h-[60vh] flex flex-col">
|
||||
<h2 class="text-lg font-semibold text-white mb-4">Your Nodes <span v-if="trustedNodes.length > 0" class="text-sm font-normal text-white/50">({{ trustedNodes.length }})</span></h2>
|
||||
|
||||
<div v-if="loading" class="flex items-center gap-3 py-8 justify-center">
|
||||
<div v-if="loading && nodes.length === 0" class="flex items-center gap-3 py-8 justify-center">
|
||||
<div class="w-5 h-5 border-2 border-white/20 border-t-orange-400 rounded-full animate-spin"></div>
|
||||
<span class="text-white/60 text-sm">Loading nodes...</span>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import DiscoverModal from '../DiscoverModal.vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
|
||||
vi.mock('@/api/rpc-client', () => ({
|
||||
rpcClient: {
|
||||
handshakeDiscover: vi.fn(),
|
||||
handshakeConnect: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void
|
||||
let reject!: (reason?: unknown) => void
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res
|
||||
reject = rej
|
||||
})
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
|
||||
function makeNode() {
|
||||
return {
|
||||
nostr_pubkey: 'pubkey-one',
|
||||
nostr_npub: 'npub1abcdefghijklmnopqrstuvwxyz',
|
||||
did: 'did:key:node',
|
||||
version: '1.8-alpha',
|
||||
}
|
||||
}
|
||||
|
||||
describe('DiscoverModal', () => {
|
||||
it('keeps discoverable nodes visible while relay refresh is pending or fails', async () => {
|
||||
vi.mocked(rpcClient.handshakeDiscover).mockResolvedValueOnce({ nodes: [makeNode()] })
|
||||
|
||||
const wrapper = mount(DiscoverModal, {
|
||||
props: { visible: false, outboundSent: [] },
|
||||
global: {
|
||||
stubs: {
|
||||
Teleport: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
await wrapper.setProps({ visible: true })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('did:key:node')
|
||||
expect(wrapper.text()).toContain('version 1.8-alpha')
|
||||
|
||||
const pending = deferred<{ nodes: [] }>()
|
||||
vi.mocked(rpcClient.handshakeDiscover).mockReturnValueOnce(pending.promise)
|
||||
|
||||
const refresh = (wrapper.vm as unknown as { refresh: () => Promise<void> }).refresh()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.text()).toContain('did:key:node')
|
||||
expect(wrapper.text()).toContain('Searching relays...')
|
||||
expect(wrapper.text()).not.toContain('No discoverable nodes found')
|
||||
|
||||
pending.reject(new Error('relay offline'))
|
||||
await refresh
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('did:key:node')
|
||||
expect(wrapper.text()).toContain('relay offline')
|
||||
expect(wrapper.text()).not.toContain('Searching relays...')
|
||||
expect(wrapper.text()).not.toContain('No discoverable nodes found')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import NodeList from '../NodeList.vue'
|
||||
import type { FederatedNode } from '../types'
|
||||
|
||||
const trustedNode: FederatedNode = {
|
||||
did: 'did:key:z6MkTrustedNode',
|
||||
pubkey: 'trusted-pubkey',
|
||||
onion: 'trusted.onion',
|
||||
trust_level: 'trusted',
|
||||
added_at: '2026-06-10T00:00:00Z',
|
||||
name: 'Trusted Node',
|
||||
last_seen: '2026-06-10T00:00:00Z',
|
||||
}
|
||||
|
||||
describe('NodeList', () => {
|
||||
it('keeps existing nodes visible during background refresh loading', () => {
|
||||
const wrapper = mount(NodeList, {
|
||||
props: {
|
||||
nodes: [trustedNode],
|
||||
loading: true,
|
||||
error: '',
|
||||
syncResults: [],
|
||||
dwnSyncDotClass: 'bg-green-400',
|
||||
cleaningNodes: false,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain('Trusted Node')
|
||||
expect(wrapper.text()).not.toContain('Loading nodes...')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user