frontend: polish app launch and release experience
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import AppSession from '../AppSession.vue'
|
||||
|
||||
const { mockReplace, mockPush, mockWindowOpen, mockSuppress, mockResume } = vi.hoisted(() => ({
|
||||
mockReplace: vi.fn(),
|
||||
mockPush: vi.fn(),
|
||||
mockWindowOpen: vi.fn(),
|
||||
mockSuppress: vi.fn(),
|
||||
mockResume: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRoute: () => ({
|
||||
params: { appId: 'gitea' },
|
||||
query: { returnTo: '/dashboard/apps' },
|
||||
fullPath: '/dashboard/apps/session/gitea',
|
||||
}),
|
||||
useRouter: () => ({ replace: mockReplace, push: mockPush }),
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/appLauncher', () => ({
|
||||
useAppLauncherStore: () => ({ panelAppId: null }),
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/app', () => ({
|
||||
useAppStore: () => ({ data: { 'package-data': {} } }),
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/screensaver', () => ({
|
||||
useScreensaverStore: () => ({ suppress: mockSuppress, resume: mockResume }),
|
||||
}))
|
||||
|
||||
vi.mock('../appSession/useAppIdentity', () => ({
|
||||
useAppIdentity: () => ({
|
||||
onIdentitySelected: vi.fn(),
|
||||
onIframeLoadIdentity: vi.fn(),
|
||||
handleIdentityRequest: vi.fn(),
|
||||
getStoredIdentity: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../appSession/useNostrBridge', () => ({
|
||||
useNostrBridge: () => ({ handleNostrRequest: vi.fn() }),
|
||||
}))
|
||||
|
||||
vi.stubGlobal('open', mockWindowOpen)
|
||||
|
||||
describe('AppSession mobile new-tab apps', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
localStorage.clear()
|
||||
Object.defineProperty(window, 'innerWidth', {
|
||||
value: 390,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { hostname: '192.168.1.228' },
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps iframe-blocked apps inside the mobile session instead of auto-opening a tab', async () => {
|
||||
const wrapper = mount(AppSession, {
|
||||
global: {
|
||||
stubs: {
|
||||
Teleport: true,
|
||||
AppSessionHeader: true,
|
||||
NostrIdentityPicker: true,
|
||||
MobileGamepad: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
expect(mockWindowOpen).not.toHaveBeenCalled()
|
||||
expect(mockReplace).not.toHaveBeenCalled()
|
||||
expect(wrapper.text()).toContain('This app opens in a new tab')
|
||||
expect(wrapper.text()).toContain('Open in new tab')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,70 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import Cloud from '../Cloud.vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRouter: () => ({ push: vi.fn() }),
|
||||
RouterLink: { name: 'RouterLink', props: ['to'], template: '<a><slot /></a>' },
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/app', () => ({
|
||||
useAppStore: () => ({ packages: {} }),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/rpc-client', () => ({
|
||||
rpcClient: {
|
||||
federationListNodes: 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 makePeer() {
|
||||
return {
|
||||
did: 'did:key:peer',
|
||||
pubkey: 'peer',
|
||||
onion: 'peer.onion',
|
||||
name: 'Peer Alpha',
|
||||
trust_level: 'trusted',
|
||||
}
|
||||
}
|
||||
|
||||
describe('Cloud peer list', () => {
|
||||
it('keeps peer nodes visible while refresh is pending or fails', async () => {
|
||||
vi.mocked(rpcClient.federationListNodes).mockResolvedValueOnce({ nodes: [makePeer()] })
|
||||
|
||||
const wrapper = mount(Cloud)
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('Peer Alpha')
|
||||
expect(wrapper.text()).not.toContain('No peers yet')
|
||||
|
||||
const pending = deferred<{ nodes: [] }>()
|
||||
vi.mocked(rpcClient.federationListNodes).mockReturnValueOnce(pending.promise)
|
||||
|
||||
const refresh = (wrapper.vm as unknown as { loadPeers: () => Promise<void> }).loadPeers()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.text()).toContain('Peer Alpha')
|
||||
expect(wrapper.text()).toContain('Refreshing peer nodes...')
|
||||
expect(wrapper.text()).not.toContain('No peers yet')
|
||||
|
||||
pending.reject(new Error('offline'))
|
||||
await refresh
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('Peer Alpha')
|
||||
expect(wrapper.text()).toContain('offline')
|
||||
expect(wrapper.text()).not.toContain('Refreshing peer nodes...')
|
||||
expect(wrapper.text()).not.toContain('No peers yet')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,76 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import Credentials from '../Credentials.vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
|
||||
vi.mock('@/api/rpc-client', () => ({
|
||||
rpcClient: {
|
||||
call: 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 makeCredential(id: string) {
|
||||
return {
|
||||
id,
|
||||
type: ['VerifiableCredential', 'NodeOperator'],
|
||||
issuer: 'did:key:issuer',
|
||||
credentialSubject: { id: 'did:key:subject' },
|
||||
issuanceDate: '2026-06-10T10:00:00Z',
|
||||
status: 'active',
|
||||
}
|
||||
}
|
||||
|
||||
describe('Credentials', () => {
|
||||
it('keeps credentials visible while refresh is pending or fails', async () => {
|
||||
vi.mocked(rpcClient.call).mockImplementation((request: { method: string }) => {
|
||||
if (request.method === 'identity.list') return Promise.resolve({ identities: [] })
|
||||
if (request.method === 'identity.list-credentials') {
|
||||
return Promise.resolve({ credentials: [makeCredential('cred-one')] })
|
||||
}
|
||||
return Promise.resolve({})
|
||||
})
|
||||
|
||||
const wrapper = mount(Credentials, {
|
||||
global: {
|
||||
mocks: {
|
||||
$router: { push: vi.fn() },
|
||||
},
|
||||
},
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('NodeOperator')
|
||||
expect(wrapper.text()).toContain('cred-one')
|
||||
|
||||
const pending = deferred<{ credentials: [] }>()
|
||||
vi.mocked(rpcClient.call).mockImplementation((request: { method: string }) => {
|
||||
if (request.method === 'identity.list-credentials') return pending.promise
|
||||
return Promise.resolve({ identities: [] })
|
||||
})
|
||||
|
||||
const refresh = (wrapper.vm as unknown as { loadCredentials: () => Promise<void> }).loadCredentials()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.text()).toContain('NodeOperator')
|
||||
expect(wrapper.text()).toContain('cred-one')
|
||||
expect(wrapper.text()).toContain('Refreshing credentials...')
|
||||
|
||||
pending.reject(new Error('offline'))
|
||||
await refresh
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('NodeOperator')
|
||||
expect(wrapper.text()).toContain('cred-one')
|
||||
expect(wrapper.text()).not.toContain('Refreshing credentials...')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import OnboardingOptions from '../OnboardingOptions.vue'
|
||||
|
||||
const push = vi.fn(() => Promise.resolve())
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRouter: () => ({ push }),
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useNavSounds', () => ({
|
||||
playNavSound: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('OnboardingOptions', () => {
|
||||
it('shows only usable setup paths', () => {
|
||||
const wrapper = mount(OnboardingOptions)
|
||||
|
||||
expect(wrapper.text()).toContain('Fresh Start')
|
||||
expect(wrapper.text()).toContain('Restore from Seed')
|
||||
expect(wrapper.text()).not.toContain('Connect Existing')
|
||||
expect(wrapper.text()).not.toContain('Coming Soon')
|
||||
})
|
||||
|
||||
it('continues to fresh seed generation by default', async () => {
|
||||
push.mockClear()
|
||||
const wrapper = mount(OnboardingOptions)
|
||||
|
||||
await wrapper.get('button.path-action-button').trigger('click')
|
||||
|
||||
expect(push).toHaveBeenCalledWith('/onboarding/seed')
|
||||
})
|
||||
|
||||
it('routes restore choice to seed restore', async () => {
|
||||
push.mockClear()
|
||||
const wrapper = mount(OnboardingOptions)
|
||||
|
||||
const restoreButton = wrapper.findAll('button').find((button) => button.text().includes('Restore from Seed'))
|
||||
expect(restoreButton).toBeDefined()
|
||||
|
||||
await restoreButton!.trigger('click')
|
||||
await wrapper.get('button.path-action-button').trigger('click')
|
||||
|
||||
expect(push).toHaveBeenCalledWith('/onboarding/seed-restore')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,85 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import PeerFiles from '../PeerFiles.vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRouter: () => ({ push: vi.fn() }),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/rpc-client', () => ({
|
||||
rpcClient: {
|
||||
call: vi.fn(),
|
||||
federationListNodes: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useAudioPlayer', () => ({
|
||||
useAudioPlayer: () => ({ play: 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 makeCatalogItem() {
|
||||
return {
|
||||
id: 'file-1',
|
||||
filename: 'notes.txt',
|
||||
mime_type: 'text/plain',
|
||||
size_bytes: 128,
|
||||
description: '',
|
||||
access: 'free',
|
||||
}
|
||||
}
|
||||
|
||||
describe('PeerFiles', () => {
|
||||
it('keeps peer catalog items visible while refresh is pending or fails', async () => {
|
||||
vi.mocked(rpcClient.federationListNodes).mockResolvedValue({
|
||||
nodes: [{
|
||||
did: 'did:key:peer',
|
||||
pubkey: 'peer',
|
||||
onion: 'peer.onion',
|
||||
name: 'Peer',
|
||||
trust_level: 'trusted',
|
||||
}],
|
||||
} as never)
|
||||
vi.mocked(rpcClient.call).mockResolvedValueOnce({ items: [makeCatalogItem()] })
|
||||
|
||||
const wrapper = mount(PeerFiles, {
|
||||
props: { peerId: 'peer.onion' },
|
||||
global: {
|
||||
stubs: {
|
||||
Teleport: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('notes.txt')
|
||||
|
||||
const pending = deferred<{ items: [] }>()
|
||||
vi.mocked(rpcClient.call).mockReturnValueOnce(pending.promise)
|
||||
|
||||
const refresh = (wrapper.vm as unknown as { loadCatalog: () => Promise<void> }).loadCatalog()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.text()).toContain('notes.txt')
|
||||
expect(wrapper.text()).toContain('Refreshing peer files...')
|
||||
expect(wrapper.text()).not.toContain('Connecting via Tor')
|
||||
|
||||
pending.reject(new Error('offline'))
|
||||
await refresh
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('notes.txt')
|
||||
expect(wrapper.text()).toContain('offline')
|
||||
expect(wrapper.text()).not.toContain('Refreshing peer files...')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,216 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import Server from '../Server.vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
|
||||
vi.mock('@/stores/app', () => ({
|
||||
useAppStore: () => ({ packages: {} }),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/rpc-client', () => ({
|
||||
rpcClient: {
|
||||
call: vi.fn(),
|
||||
vpnStatus: vi.fn(),
|
||||
dnsStatus: vi.fn(),
|
||||
diskStatus: 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 mountServer(options: { renderTorServices?: boolean } = {}) {
|
||||
return mount(Server, {
|
||||
global: {
|
||||
stubs: {
|
||||
QuickActionsCard: true,
|
||||
TorServicesCard: options.renderTorServices ? false : true,
|
||||
ServerModals: true,
|
||||
FipsNetworkCard: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('Server network refresh states', () => {
|
||||
it('keeps network overview visible while refresh is pending', async () => {
|
||||
vi.mocked(rpcClient.call).mockImplementation((request: { method: string }) => {
|
||||
if (request.method === 'network.diagnostics') {
|
||||
return Promise.resolve({ tor_connected: true, wifi_count: 2, wifi_ssid: 'Lab WiFi' })
|
||||
}
|
||||
if (request.method === 'router.list-forwards') {
|
||||
return Promise.resolve({ forwards: [{}, {}] })
|
||||
}
|
||||
if (request.method === 'network.list-interfaces') {
|
||||
return Promise.resolve({ interfaces: [] })
|
||||
}
|
||||
if (request.method === 'tor.list-services') {
|
||||
return Promise.resolve({ services: [], tor_running: false })
|
||||
}
|
||||
if (request.method === 'vpn.list-peers') {
|
||||
return Promise.resolve({ peers: [] })
|
||||
}
|
||||
if (request.method === 'fips.status') {
|
||||
return Promise.resolve({ installed: false, service_active: false, key_present: false })
|
||||
}
|
||||
return Promise.resolve({})
|
||||
})
|
||||
vi.mocked(rpcClient.vpnStatus).mockResolvedValue({ connected: true, provider: 'wireguard', ip_address: '10.0.0.2/32', wg_ip: '10.0.0.1/24' } as never)
|
||||
vi.mocked(rpcClient.dnsStatus).mockResolvedValue({ provider: 'cloudflare', resolv_conf_servers: ['1.1.1.1'], doh_enabled: true } as never)
|
||||
vi.mocked(rpcClient.diskStatus).mockResolvedValue({ encrypted: false, warnings: [] } as never)
|
||||
|
||||
const wrapper = mountServer()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('Lab WiFi')
|
||||
expect(wrapper.text()).toContain('2 rules')
|
||||
|
||||
const pendingDiagnostics = deferred<{ tor_connected: boolean; wifi_count: number; wifi_ssid: string }>()
|
||||
vi.mocked(rpcClient.call).mockImplementation((request: { method: string }) => {
|
||||
if (request.method === 'network.diagnostics') return pendingDiagnostics.promise
|
||||
if (request.method === 'router.list-forwards') return Promise.reject(new Error('offline'))
|
||||
return Promise.resolve({})
|
||||
})
|
||||
vi.mocked(rpcClient.vpnStatus).mockRejectedValueOnce(new Error('offline') as never)
|
||||
vi.mocked(rpcClient.dnsStatus).mockRejectedValueOnce(new Error('offline') as never)
|
||||
|
||||
const refresh = (wrapper.vm as unknown as { loadNetworkData: () => Promise<void> }).loadNetworkData()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.text()).toContain('Lab WiFi')
|
||||
expect(wrapper.text()).toContain('2 rules')
|
||||
expect(wrapper.text()).toContain('Refreshing network...')
|
||||
|
||||
pendingDiagnostics.reject(new Error('offline'))
|
||||
await refresh
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('Lab WiFi')
|
||||
expect(wrapper.text()).toContain('2 rules')
|
||||
})
|
||||
|
||||
it('keeps network interfaces visible while refresh is pending or fails', async () => {
|
||||
vi.mocked(rpcClient.call).mockImplementation((request: { method: string }) => {
|
||||
if (request.method === 'network.list-interfaces') {
|
||||
return Promise.resolve({
|
||||
interfaces: [{ name: 'eth0', type: 'ethernet', state: 'up', mac: '00:11:22:33:44:55', ipv4: ['192.168.1.10'] }],
|
||||
})
|
||||
}
|
||||
if (request.method === 'network.diagnostics') {
|
||||
return Promise.resolve({ tor_connected: false })
|
||||
}
|
||||
if (request.method === 'router.list-forwards') {
|
||||
return Promise.resolve({ forwards: [] })
|
||||
}
|
||||
if (request.method === 'tor.list-services') {
|
||||
return Promise.resolve({ services: [], tor_running: false })
|
||||
}
|
||||
if (request.method === 'vpn.list-peers') {
|
||||
return Promise.resolve({ peers: [] })
|
||||
}
|
||||
if (request.method === 'fips.status') {
|
||||
return Promise.resolve({ installed: false, service_active: false, key_present: false })
|
||||
}
|
||||
return Promise.resolve({})
|
||||
})
|
||||
vi.mocked(rpcClient.vpnStatus).mockResolvedValue({ connected: false } as never)
|
||||
vi.mocked(rpcClient.dnsStatus).mockResolvedValue({ provider: 'system', resolv_conf_servers: [], doh_enabled: false } as never)
|
||||
vi.mocked(rpcClient.diskStatus).mockResolvedValue({ encrypted: false, warnings: [] } as never)
|
||||
|
||||
const wrapper = mountServer()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('eth0')
|
||||
expect(wrapper.text()).toContain('192.168.1.10')
|
||||
|
||||
const pendingInterfaces = deferred<{ interfaces: [] }>()
|
||||
vi.mocked(rpcClient.call).mockImplementation((request: { method: string }) => {
|
||||
if (request.method === 'network.list-interfaces') return pendingInterfaces.promise
|
||||
return Promise.resolve({})
|
||||
})
|
||||
|
||||
const refresh = (wrapper.vm as unknown as { loadInterfaces: () => Promise<void> }).loadInterfaces()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.text()).toContain('eth0')
|
||||
expect(wrapper.text()).toContain('192.168.1.10')
|
||||
expect(wrapper.text()).toContain('Refreshing interfaces...')
|
||||
|
||||
pendingInterfaces.reject(new Error('offline'))
|
||||
await refresh
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('eth0')
|
||||
expect(wrapper.text()).toContain('192.168.1.10')
|
||||
})
|
||||
|
||||
it('keeps Tor services visible while refresh is pending or fails', async () => {
|
||||
vi.mocked(rpcClient.call).mockImplementation((request: { method: string }) => {
|
||||
if (request.method === 'tor.list-services') {
|
||||
return Promise.resolve({
|
||||
services: [{
|
||||
name: 'filebrowser',
|
||||
local_port: 8080,
|
||||
onion_address: 'filebrowser123456789.onion',
|
||||
enabled: true,
|
||||
unauthenticated: false,
|
||||
protocol: false,
|
||||
}],
|
||||
tor_running: true,
|
||||
})
|
||||
}
|
||||
if (request.method === 'network.diagnostics') {
|
||||
return Promise.resolve({ tor_connected: true })
|
||||
}
|
||||
if (request.method === 'router.list-forwards') {
|
||||
return Promise.resolve({ forwards: [] })
|
||||
}
|
||||
if (request.method === 'network.list-interfaces') {
|
||||
return Promise.resolve({ interfaces: [] })
|
||||
}
|
||||
if (request.method === 'vpn.list-peers') {
|
||||
return Promise.resolve({ peers: [] })
|
||||
}
|
||||
if (request.method === 'fips.status') {
|
||||
return Promise.resolve({ installed: false, service_active: false, key_present: false })
|
||||
}
|
||||
return Promise.resolve({})
|
||||
})
|
||||
vi.mocked(rpcClient.vpnStatus).mockResolvedValue({ connected: false } as never)
|
||||
vi.mocked(rpcClient.dnsStatus).mockResolvedValue({ provider: 'system', resolv_conf_servers: [], doh_enabled: false } as never)
|
||||
vi.mocked(rpcClient.diskStatus).mockResolvedValue({ encrypted: false, warnings: [] } as never)
|
||||
|
||||
const wrapper = mountServer({ renderTorServices: true })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('filebrowser')
|
||||
expect(wrapper.text()).toContain('filebrowser123456789.onion')
|
||||
|
||||
const pendingTor = deferred<{ services: []; tor_running: boolean }>()
|
||||
vi.mocked(rpcClient.call).mockImplementation((request: { method: string }) => {
|
||||
if (request.method === 'tor.list-services') return pendingTor.promise
|
||||
return Promise.resolve({})
|
||||
})
|
||||
|
||||
const refresh = (wrapper.vm as unknown as { loadTorServices: () => Promise<void> }).loadTorServices()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.text()).toContain('filebrowser')
|
||||
expect(wrapper.text()).toContain('filebrowser123456789.onion')
|
||||
expect(wrapper.text()).toContain('Refreshing Tor services...')
|
||||
|
||||
pendingTor.reject(new Error('offline'))
|
||||
await refresh
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('filebrowser')
|
||||
expect(wrapper.text()).toContain('filebrowser123456789.onion')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { normalizeCloudPath, parentCloudPath } from '../cloudPath'
|
||||
|
||||
describe('cloudPath helpers', () => {
|
||||
it('normalizes query paths', () => {
|
||||
expect(normalizeCloudPath('Photos/Trips')).toBe('/Photos/Trips')
|
||||
expect(normalizeCloudPath('/Photos//Trips')).toBe('/Photos/Trips')
|
||||
expect(normalizeCloudPath('', '/Photos')).toBe('/Photos')
|
||||
})
|
||||
|
||||
it('walks to the parent folder without leaving root', () => {
|
||||
expect(parentCloudPath('/Photos/Trips/Day 1')).toBe('/Photos/Trips')
|
||||
expect(parentCloudPath('/Photos')).toBe('/')
|
||||
expect(parentCloudPath('/')).toBe('/')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user