- Mesh: fix 920-1280px bottom margin (phantom mobile-nav reservation leaking into the desktop-sidebar range), let the mesh view scale to full width on wide screens instead of capping at 1600px, and make the Device panel collapsible on desktop (previously mobile-only) - Search/controller-nav: a global gamepad/keyboard-nav feature was auto-clicking "the next button in the DOM" on Enter in any text input, which cleared the mesh peer search and popped the sideload modal from the App Store/My Apps search boxes. Opt out via data-controller-no-submit on all filter inputs; bump the mesh clear button's touch target - Modals: several (sideload, credential, Lightning channel open, identity create) used ad-hoc blue buttons and non-fullscreen backdrops that only covered the main content area, not the sidebar. Teleport them to body, unify backdrop/button theming to the dark+orange convention, fix the sideload modal's square bottom corners on desktop, and standardize close buttons to the ghost-icon style - Web5: remove the redundant/dead "Messages" tab from Connected Nodes (its deep-link was unreachable dead code); fix the "view message" toast to actually open the Archipelago channel instead of silently failing to match a LoRa peer; make identity rows responsive via a container query (viewport-based breakpoints don't work in the page's 2-column grid) and right-justify their action icons; collapse DID/DHT/Wallet/Nostr/Connected Nodes by default on mobile - Apps/App Store: match the search bar and sideload button's height, padding, and background to the mode-switcher tabs beside them - Mesh chat: keep the compose input focused after sending Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
186 lines
5.3 KiB
TypeScript
186 lines
5.3 KiB
TypeScript
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
|
import { flushPromises, mount } from '@vue/test-utils'
|
|
import { createPinia, setActivePinia } from 'pinia'
|
|
import { PackageState, type PackageDataEntry } from '@/types/api'
|
|
import { useAppLauncherStore } from '@/stores/appLauncher'
|
|
import { useServerStore } from '@/stores/server'
|
|
import AppIconGrid from '../AppIconGrid.vue'
|
|
|
|
const mockWindowOpen = vi.fn()
|
|
|
|
vi.mock('@/api/rpc-client', () => ({
|
|
rpcClient: {
|
|
call: vi.fn().mockResolvedValue({ credentials: [] }),
|
|
},
|
|
}))
|
|
|
|
vi.stubGlobal('open', mockWindowOpen)
|
|
|
|
function makePkg(id: string): PackageDataEntry {
|
|
return {
|
|
state: PackageState.Running,
|
|
manifest: {
|
|
id,
|
|
title: id,
|
|
version: '1.0.0',
|
|
description: { short: '', long: '' },
|
|
'release-notes': '',
|
|
license: '',
|
|
'wrapper-repo': '',
|
|
'upstream-repo': '',
|
|
'support-site': '',
|
|
'marketing-site': '',
|
|
'donation-url': null,
|
|
interfaces: { main: { ui: true } },
|
|
} as unknown as PackageDataEntry['manifest'],
|
|
'static-files': { license: '', instructions: '', icon: '' },
|
|
}
|
|
}
|
|
|
|
describe('AppIconGrid', () => {
|
|
let pinia: ReturnType<typeof createPinia>
|
|
|
|
beforeEach(() => {
|
|
vi.useRealTimers()
|
|
pinia = createPinia()
|
|
setActivePinia(pinia)
|
|
vi.clearAllMocks()
|
|
localStorage.clear()
|
|
Object.defineProperty(window, 'innerWidth', {
|
|
value: 1024,
|
|
writable: true,
|
|
configurable: true,
|
|
})
|
|
Object.defineProperty(window, 'location', {
|
|
value: { hostname: '192.168.1.198' },
|
|
writable: true,
|
|
configurable: true,
|
|
})
|
|
})
|
|
|
|
it('opens LND companion UI in the app panel', async () => {
|
|
const wrapper = mount(AppIconGrid, {
|
|
props: { apps: [['lnd', makePkg('lnd')]] },
|
|
global: {
|
|
plugins: [pinia],
|
|
},
|
|
})
|
|
|
|
await wrapper.get('.app-icon-item').trigger('click')
|
|
await flushPromises()
|
|
|
|
expect(mockWindowOpen).not.toHaveBeenCalled()
|
|
expect(useAppLauncherStore(pinia).panelAppId).toBe('lnd')
|
|
})
|
|
|
|
it('shows File Browser credentials before launch even when backend returns no credentials', async () => {
|
|
const wrapper = mount(AppIconGrid, {
|
|
props: { apps: [['filebrowser', makePkg('filebrowser')]] },
|
|
global: {
|
|
plugins: [pinia],
|
|
// The credential modal is <Teleport to="body">'d (so its full-screen
|
|
// backdrop isn't clipped by the dashboard's transformed layout) —
|
|
// stub it to render inline so wrapper.text() still sees it.
|
|
stubs: { teleport: true },
|
|
},
|
|
})
|
|
|
|
await wrapper.get('.app-icon-item').trigger('click')
|
|
await flushPromises()
|
|
|
|
expect(wrapper.text()).toContain('File Browser credentials')
|
|
expect(wrapper.text()).toContain('Username')
|
|
expect(wrapper.text()).toContain('admin')
|
|
expect(useAppLauncherStore(pinia).panelAppId).toBeNull()
|
|
})
|
|
|
|
it('opens unresolved new-tab apps externally on mobile', async () => {
|
|
Object.defineProperty(window, 'innerWidth', {
|
|
value: 390,
|
|
writable: true,
|
|
configurable: true,
|
|
})
|
|
|
|
const wrapper = mount(AppIconGrid, {
|
|
props: { apps: [['gitea', makePkg('gitea')]] },
|
|
global: {
|
|
plugins: [pinia],
|
|
},
|
|
})
|
|
|
|
await wrapper.get('.app-icon-item').trigger('click')
|
|
await flushPromises()
|
|
|
|
expect(mockWindowOpen).toHaveBeenCalledWith(
|
|
'http://192.168.1.198:3001',
|
|
'_blank',
|
|
'noopener,noreferrer',
|
|
)
|
|
expect(useAppLauncherStore(pinia).panelAppId).toBeNull()
|
|
})
|
|
|
|
it('shows backend uninstall stage while an app is removing', () => {
|
|
const pkg = makePkg('indeedhub')
|
|
pkg.state = PackageState.Removing
|
|
pkg['uninstall-stage'] = 'Stopping containers (2/7)'
|
|
useServerStore(pinia).uninstallingApps.add('indeedhub')
|
|
|
|
const wrapper = mount(AppIconGrid, {
|
|
props: { apps: [['indeedhub', pkg]] },
|
|
global: {
|
|
plugins: [pinia],
|
|
},
|
|
})
|
|
|
|
expect(wrapper.text()).toContain('Stopping containers (2/7)')
|
|
})
|
|
|
|
it('supports legacy underscore uninstall stage data', () => {
|
|
const pkg = makePkg('indeedhub')
|
|
pkg.state = PackageState.Removing
|
|
;(pkg as PackageDataEntry & { uninstall_stage?: string }).uninstall_stage = 'Removing app data'
|
|
useServerStore(pinia).uninstallingApps.add('indeedhub')
|
|
|
|
const wrapper = mount(AppIconGrid, {
|
|
props: { apps: [['indeedhub', pkg]] },
|
|
global: {
|
|
plugins: [pinia],
|
|
},
|
|
})
|
|
|
|
expect(wrapper.text()).toContain('Removing app data')
|
|
})
|
|
|
|
it('opens app details on long press without launching the app', async () => {
|
|
vi.useFakeTimers()
|
|
const wrapper = mount(AppIconGrid, {
|
|
props: { apps: [['lnd', makePkg('lnd')]] },
|
|
global: {
|
|
plugins: [pinia],
|
|
},
|
|
})
|
|
|
|
const icon = wrapper.get('.app-icon-item')
|
|
await icon.trigger('pointerdown')
|
|
vi.advanceTimersByTime(550)
|
|
await icon.trigger('click')
|
|
await flushPromises()
|
|
|
|
expect(wrapper.emitted('goToApp')).toEqual([['lnd']])
|
|
expect(useAppLauncherStore(pinia).panelAppId).toBeNull()
|
|
})
|
|
|
|
it('opens app details from the keyboard options shortcut', async () => {
|
|
const wrapper = mount(AppIconGrid, {
|
|
props: { apps: [['lnd', makePkg('lnd')]] },
|
|
global: {
|
|
plugins: [pinia],
|
|
},
|
|
})
|
|
|
|
await wrapper.get('.app-icon-item').trigger('keydown.space')
|
|
|
|
expect(wrapper.emitted('goToApp')).toEqual([['lnd']])
|
|
})
|
|
})
|