test: fix all 10 failing frontend tests
Updated appLauncher tests to match current session-based routing. Fixed settings test to use h2 instead of h1. Fixed RPC client test to expect 'Session expired' on 401. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b91d4c0169
commit
f25ee5a00b
@ -86,7 +86,7 @@
|
|||||||
|
|
||||||
- [x] **Remove dead dockerode dependency**: Run `cd /Users/dorian/Projects/archy/neode-ui && npm uninstall dockerode` and `npm uninstall @types/dockerode` if it exists. Search the codebase for any remaining imports: `grep -r "dockerode" neode-ui/src/`. Remove any dead imports found. Run `npm run type-check` to verify nothing breaks.
|
- [x] **Remove dead dockerode dependency**: Run `cd /Users/dorian/Projects/archy/neode-ui && npm uninstall dockerode` and `npm uninstall @types/dockerode` if it exists. Search the codebase for any remaining imports: `grep -r "dockerode" neode-ui/src/`. Remove any dead imports found. Run `npm run type-check` to verify nothing breaks.
|
||||||
|
|
||||||
- [ ] **Fix the 10 failing frontend tests**: Run `cd /Users/dorian/Projects/archy/neode-ui && npm run test -- --reporter=verbose 2>&1 | head -100` to see which tests fail. Known failures: (1) `src/stores/__tests__/appLauncher.test.ts` — URL rewriting tests expecting different proxy behavior, (2) `src/views/__tests__/settings.test.ts` — heading selector `h1` not finding the heading element. For each failing test, read the test file and the component/store it tests. Update test expectations to match current implementation. Do NOT change the production code to match tests — fix the tests. Run `npm run test` until all pass.
|
- [x] **Fix the 10 failing frontend tests**: Run `cd /Users/dorian/Projects/archy/neode-ui && npm run test -- --reporter=verbose 2>&1 | head -100` to see which tests fail. Known failures: (1) `src/stores/__tests__/appLauncher.test.ts` — URL rewriting tests expecting different proxy behavior, (2) `src/views/__tests__/settings.test.ts` — heading selector `h1` not finding the heading element. For each failing test, read the test file and the component/store it tests. Update test expectations to match current implementation. Do NOT change the production code to match tests — fix the tests. Run `npm run test` until all pass.
|
||||||
|
|
||||||
- [ ] **Add 404 catch-all route**: In `neode-ui/src/router/index.ts`, add a catch-all route at the end of the routes array: `{ path: '/:pathMatch(.*)*', name: 'not-found', component: () => import('@/views/NotFound.vue') }`. Create `neode-ui/src/views/NotFound.vue` — a simple view using the existing `.glass-card` class with "Page not found" message and a router-link back to `/dashboard`. Use `<script setup lang="ts">`, no props needed. Style with existing global classes only (`.glass-card`, `.glass-button`). Run `npm run type-check`.
|
- [ ] **Add 404 catch-all route**: In `neode-ui/src/router/index.ts`, add a catch-all route at the end of the routes array: `{ path: '/:pathMatch(.*)*', name: 'not-found', component: () => import('@/views/NotFound.vue') }`. Create `neode-ui/src/views/NotFound.vue` — a simple view using the existing `.glass-card` class with "Page not found" message and a router-link back to `/dashboard`. Use `<script setup lang="ts">`, no props needed. Style with existing global classes only (`.glass-card`, `.glass-button`). Run `npm run type-check`.
|
||||||
|
|
||||||
|
|||||||
@ -98,7 +98,7 @@ describe('RPCClient', () => {
|
|||||||
it('throws immediately on non-retryable HTTP errors (e.g. 401)', async () => {
|
it('throws immediately on non-retryable HTTP errors (e.g. 401)', async () => {
|
||||||
mockFetch.mockResolvedValueOnce(jsonResponse(null, 401, 'Unauthorized'))
|
mockFetch.mockResolvedValueOnce(jsonResponse(null, 401, 'Unauthorized'))
|
||||||
|
|
||||||
await expect(rpcClient.call({ method: 'test' })).rejects.toThrow('HTTP 401: Unauthorized')
|
await expect(rpcClient.call({ method: 'test' })).rejects.toThrow('Session expired')
|
||||||
expect(mockFetch).toHaveBeenCalledOnce()
|
expect(mockFetch).toHaveBeenCalledOnce()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,24 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
import { setActivePinia, createPinia } from 'pinia'
|
import { setActivePinia, createPinia } from 'pinia'
|
||||||
|
|
||||||
import { useAppLauncherStore } from '../appLauncher'
|
// vi.hoisted runs before vi.mock hoisting
|
||||||
|
const { mockPush, mockWindowOpen } = vi.hoisted(() => ({
|
||||||
|
mockPush: vi.fn(),
|
||||||
|
mockWindowOpen: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock vue-router
|
||||||
|
vi.mock('vue-router', () => ({
|
||||||
|
useRouter: () => ({ push: mockPush }),
|
||||||
|
}))
|
||||||
|
vi.mock('@/router', () => ({
|
||||||
|
default: { push: mockPush },
|
||||||
|
}))
|
||||||
|
|
||||||
// Mock window.open for new-tab tests
|
|
||||||
const mockWindowOpen = vi.fn()
|
|
||||||
vi.stubGlobal('open', mockWindowOpen)
|
vi.stubGlobal('open', mockWindowOpen)
|
||||||
|
|
||||||
|
import { useAppLauncherStore } from '../appLauncher'
|
||||||
|
|
||||||
describe('useAppLauncherStore', () => {
|
describe('useAppLauncherStore', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setActivePinia(createPinia())
|
setActivePinia(createPinia())
|
||||||
@ -26,62 +38,60 @@ describe('useAppLauncherStore', () => {
|
|||||||
expect(store.title).toBe('')
|
expect(store.title).toBe('')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('opens an app in the iframe overlay', () => {
|
it('routes known port apps to full-page session', () => {
|
||||||
const store = useAppLauncherStore()
|
const store = useAppLauncherStore()
|
||||||
|
|
||||||
store.open({ url: 'http://192.168.1.228:8080', title: 'Mempool' })
|
// Port 8083 maps to /app/filebrowser/ — should route to session
|
||||||
|
store.open({ url: 'http://192.168.1.228:8083', title: 'FileBrowser' })
|
||||||
|
|
||||||
expect(store.isOpen).toBe(true)
|
// Legacy overlay should NOT open — routed to session view instead
|
||||||
expect(store.url).toBe('http://192.168.1.228:8080')
|
expect(store.isOpen).toBe(false)
|
||||||
expect(store.title).toBe('Mempool')
|
expect(mockPush).toHaveBeenCalledWith({ name: 'app-session', params: { appId: 'filebrowser' } })
|
||||||
expect(mockWindowOpen).not.toHaveBeenCalled()
|
expect(mockWindowOpen).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('opens BTCPay (port 23000) in a new tab due to X-Frame-Options', () => {
|
it('routes BTCPay (port 23000) to full-page session', () => {
|
||||||
const store = useAppLauncherStore()
|
const store = useAppLauncherStore()
|
||||||
|
|
||||||
store.open({ url: 'http://192.168.1.228:23000', title: 'BTCPay' })
|
store.open({ url: 'http://192.168.1.228:23000', title: 'BTCPay' })
|
||||||
|
|
||||||
expect(store.isOpen).toBe(false)
|
expect(store.isOpen).toBe(false)
|
||||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
expect(mockPush).toHaveBeenCalledWith({ name: 'app-session', params: { appId: 'btcpay' } })
|
||||||
'http://192.168.1.228:23000',
|
|
||||||
'_blank',
|
|
||||||
'noopener,noreferrer',
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('opens Home Assistant (port 8123) in a new tab', () => {
|
it('routes Home Assistant (port 8123) to full-page session', () => {
|
||||||
const store = useAppLauncherStore()
|
const store = useAppLauncherStore()
|
||||||
|
|
||||||
store.open({ url: 'http://192.168.1.228:8123', title: 'Home Assistant' })
|
store.open({ url: 'http://192.168.1.228:8123', title: 'Home Assistant' })
|
||||||
|
|
||||||
expect(store.isOpen).toBe(false)
|
expect(store.isOpen).toBe(false)
|
||||||
expect(mockWindowOpen).toHaveBeenCalled()
|
expect(mockPush).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('opens Grafana (port 3000) in a new tab', () => {
|
it('routes Grafana (port 3000) to full-page session', () => {
|
||||||
const store = useAppLauncherStore()
|
const store = useAppLauncherStore()
|
||||||
|
|
||||||
store.open({ url: 'http://192.168.1.228:3000', title: 'Grafana' })
|
store.open({ url: 'http://192.168.1.228:3000', title: 'Grafana' })
|
||||||
|
|
||||||
expect(store.isOpen).toBe(false)
|
expect(store.isOpen).toBe(false)
|
||||||
expect(mockWindowOpen).toHaveBeenCalled()
|
expect(mockPush).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('opens in new tab when openInNewTab flag is set', () => {
|
it('opens in new tab when openInNewTab flag is set for unknown URL', () => {
|
||||||
const store = useAppLauncherStore()
|
const store = useAppLauncherStore()
|
||||||
|
|
||||||
store.open({ url: 'http://192.168.1.228:8080', title: 'Mempool', openInNewTab: true })
|
// Use an unresolvable URL so it doesn't route to session
|
||||||
|
store.open({ url: 'http://192.168.1.228:9999', title: 'Unknown', openInNewTab: true })
|
||||||
|
|
||||||
expect(store.isOpen).toBe(false)
|
expect(store.isOpen).toBe(false)
|
||||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||||
'http://192.168.1.228:8080',
|
'http://192.168.1.228:9999',
|
||||||
'_blank',
|
'_blank',
|
||||||
'noopener,noreferrer',
|
'noopener,noreferrer',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('rewrites URL to proxy path on HTTPS for same-host apps', () => {
|
it('routes HTTPS same-host apps via session view', () => {
|
||||||
Object.defineProperty(window, 'location', {
|
Object.defineProperty(window, 'location', {
|
||||||
value: { origin: 'https://192.168.1.228', protocol: 'https:', hostname: '192.168.1.228' },
|
value: { origin: 'https://192.168.1.228', protocol: 'https:', hostname: '192.168.1.228' },
|
||||||
writable: true,
|
writable: true,
|
||||||
@ -91,19 +101,24 @@ describe('useAppLauncherStore', () => {
|
|||||||
|
|
||||||
store.open({ url: 'http://192.168.1.228:8083', title: 'FileBrowser' })
|
store.open({ url: 'http://192.168.1.228:8083', title: 'FileBrowser' })
|
||||||
|
|
||||||
|
// Known port — routes to full-page session
|
||||||
|
expect(store.isOpen).toBe(false)
|
||||||
|
expect(mockPush).toHaveBeenCalledWith({ name: 'app-session', params: { appId: 'filebrowser' } })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opens unknown URL in iframe overlay on HTTP', () => {
|
||||||
|
const store = useAppLauncherStore()
|
||||||
|
|
||||||
|
// Unresolvable URL — falls through to iframe overlay
|
||||||
|
store.open({ url: 'http://192.168.1.228:9999', title: 'Custom App' })
|
||||||
|
|
||||||
expect(store.isOpen).toBe(true)
|
expect(store.isOpen).toBe(true)
|
||||||
expect(store.url).toBe('https://192.168.1.228/app/filebrowser/')
|
expect(store.url).toBe('http://192.168.1.228:9999')
|
||||||
|
expect(store.title).toBe('Custom App')
|
||||||
|
expect(mockWindowOpen).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not rewrite URL on HTTP (no mixed content)', () => {
|
it('opens unknown different-host URL in iframe overlay', () => {
|
||||||
const store = useAppLauncherStore()
|
|
||||||
|
|
||||||
store.open({ url: 'http://192.168.1.228:8083', title: 'FileBrowser' })
|
|
||||||
|
|
||||||
expect(store.url).toBe('http://192.168.1.228:8083')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('does not rewrite URL for different hosts', () => {
|
|
||||||
Object.defineProperty(window, 'location', {
|
Object.defineProperty(window, 'location', {
|
||||||
value: { origin: 'https://192.168.1.228', protocol: 'https:', hostname: '192.168.1.228' },
|
value: { origin: 'https://192.168.1.228', protocol: 'https:', hostname: '192.168.1.228' },
|
||||||
writable: true,
|
writable: true,
|
||||||
@ -111,15 +126,17 @@ describe('useAppLauncherStore', () => {
|
|||||||
})
|
})
|
||||||
const store = useAppLauncherStore()
|
const store = useAppLauncherStore()
|
||||||
|
|
||||||
store.open({ url: 'http://192.168.1.100:8083', title: 'Remote FileBrowser' })
|
store.open({ url: 'http://192.168.1.100:9999', title: 'Remote App' })
|
||||||
|
|
||||||
// Different host — no proxy rewriting
|
// Different host, unknown port — opens in iframe overlay (no proxy rewrite)
|
||||||
expect(store.url).toBe('http://192.168.1.100:8083')
|
expect(store.isOpen).toBe(true)
|
||||||
|
expect(store.url).toBe('http://192.168.1.100:9999')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('close resets state', () => {
|
it('close resets state', () => {
|
||||||
const store = useAppLauncherStore()
|
const store = useAppLauncherStore()
|
||||||
store.open({ url: 'http://192.168.1.228:8080', title: 'Mempool' })
|
// Use unknown URL to trigger iframe overlay
|
||||||
|
store.open({ url: 'http://192.168.1.228:9999', title: 'Custom' })
|
||||||
|
|
||||||
store.close()
|
store.close()
|
||||||
|
|
||||||
@ -134,7 +151,7 @@ describe('useAppLauncherStore', () => {
|
|||||||
const mockButton = { focus: vi.fn() } as unknown as HTMLElement
|
const mockButton = { focus: vi.fn() } as unknown as HTMLElement
|
||||||
Object.defineProperty(document, 'activeElement', { value: mockButton, configurable: true })
|
Object.defineProperty(document, 'activeElement', { value: mockButton, configurable: true })
|
||||||
|
|
||||||
store.open({ url: 'http://192.168.1.228:8080', title: 'Mempool' })
|
store.open({ url: 'http://192.168.1.228:9999', title: 'Custom' })
|
||||||
store.close()
|
store.close()
|
||||||
|
|
||||||
expect(store.isOpen).toBe(false)
|
expect(store.isOpen).toBe(false)
|
||||||
|
|||||||
@ -130,11 +130,11 @@ describe('Settings View', () => {
|
|||||||
expect(wrapper.exists()).toBe(true)
|
expect(wrapper.exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('displays the Settings heading', () => {
|
it('displays the Account section heading', () => {
|
||||||
const wrapper = mountSettings()
|
const wrapper = mountSettings()
|
||||||
const heading = wrapper.find('h1')
|
const heading = wrapper.find('h2')
|
||||||
expect(heading.exists()).toBe(true)
|
expect(heading.exists()).toBe(true)
|
||||||
expect(heading.text()).toBe('Settings')
|
expect(heading.text()).toBe('Account')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('displays the Account section with server name and version', () => {
|
it('displays the Account section with server name and version', () => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user