initial commit
This commit is contained in:
85
test/nuxt/CameraViewer.spec.js
Normal file
85
test/nuxt/CameraViewer.spec.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mountSuspended } from '@nuxt/test-utils/runtime'
|
||||
import CameraViewer from '../../app/components/CameraViewer.vue'
|
||||
|
||||
describe('CameraViewer (device stream)', () => {
|
||||
it('renders device name and close button', async () => {
|
||||
const camera = {
|
||||
id: 't1',
|
||||
name: 'Test Camera',
|
||||
streamUrl: 'https://example.com/stream.mjpg',
|
||||
sourceType: 'mjpeg',
|
||||
}
|
||||
const wrapper = await mountSuspended(CameraViewer, {
|
||||
props: { camera },
|
||||
})
|
||||
expect(wrapper.text()).toContain('Test Camera')
|
||||
expect(wrapper.find('button[aria-label="Close panel"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('does not set img src for non-http streamUrl', async () => {
|
||||
const camera = {
|
||||
id: 't2',
|
||||
name: 'Bad',
|
||||
streamUrl: 'javascript:alert(1)',
|
||||
sourceType: 'mjpeg',
|
||||
}
|
||||
const wrapper = await mountSuspended(CameraViewer, {
|
||||
props: { camera },
|
||||
})
|
||||
const img = wrapper.find('img')
|
||||
expect(img.exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('uses safe http streamUrl for img', async () => {
|
||||
const camera = {
|
||||
id: 't3',
|
||||
name: 'OK',
|
||||
streamUrl: 'https://example.com/cam.mjpg',
|
||||
sourceType: 'mjpeg',
|
||||
}
|
||||
const wrapper = await mountSuspended(CameraViewer, {
|
||||
props: { camera },
|
||||
})
|
||||
const img = wrapper.find('img')
|
||||
expect(img.exists()).toBe(true)
|
||||
expect(img.attributes('src')).toBe('https://example.com/cam.mjpg')
|
||||
})
|
||||
|
||||
it('emits close when close button clicked', async () => {
|
||||
const camera = {
|
||||
id: 't5',
|
||||
name: 'Close me',
|
||||
streamUrl: '',
|
||||
sourceType: 'mjpeg',
|
||||
}
|
||||
const wrapper = await mountSuspended(CameraViewer, { props: { camera } })
|
||||
await wrapper.find('button[aria-label="Close panel"]').trigger('click')
|
||||
expect(wrapper.emitted('close')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('shows stream unavailable when img errors', async () => {
|
||||
const camera = {
|
||||
id: 't6',
|
||||
name: 'Broken',
|
||||
streamUrl: 'https://example.com/bad.mjpg',
|
||||
sourceType: 'mjpeg',
|
||||
}
|
||||
const wrapper = await mountSuspended(CameraViewer, { props: { camera } })
|
||||
const img = wrapper.find('img')
|
||||
await img.trigger('error')
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.text()).toContain('Stream unavailable')
|
||||
})
|
||||
|
||||
it('renders video element for hls sourceType', async () => {
|
||||
const camera = {
|
||||
id: 't7',
|
||||
name: 'HLS Camera',
|
||||
streamUrl: 'https://example.com/stream.m3u8',
|
||||
sourceType: 'hls',
|
||||
}
|
||||
const wrapper = await mountSuspended(CameraViewer, { props: { camera } })
|
||||
expect(wrapper.find('video').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
78
test/nuxt/KestrelMap.spec.js
Normal file
78
test/nuxt/KestrelMap.spec.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { mountSuspended } from '@nuxt/test-utils/runtime'
|
||||
import KestrelMap from '../../app/components/KestrelMap.vue'
|
||||
|
||||
vi.mock('leaflet', () => ({ default: {} }))
|
||||
vi.mock('leaflet.offline', () => ({ tileLayerOffline: null, savetiles: null }))
|
||||
|
||||
describe('KestrelMap', () => {
|
||||
it('renders map container', async () => {
|
||||
const wrapper = await mountSuspended(KestrelMap, {
|
||||
props: { feeds: [] },
|
||||
})
|
||||
expect(wrapper.find('[data-testid="kestrel-map"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts feeds prop', async () => {
|
||||
const feeds = [
|
||||
{ id: '1', name: 'A', lat: 37.7, lng: -122.4, streamUrl: '', sourceType: 'mjpeg' },
|
||||
]
|
||||
const wrapper = await mountSuspended(KestrelMap, {
|
||||
props: { feeds },
|
||||
})
|
||||
expect(wrapper.props('feeds')).toEqual(feeds)
|
||||
})
|
||||
|
||||
it('has select emit', async () => {
|
||||
const wrapper = await mountSuspended(KestrelMap, {
|
||||
props: { feeds: [] },
|
||||
})
|
||||
wrapper.vm.$emit('select', { id: 'x', name: 'X', lat: 0, lng: 0 })
|
||||
expect(wrapper.emitted('select')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('uses dark Carto tile URL with subdomains', async () => {
|
||||
const componentPath = resolve(__dirname, '../../app/components/KestrelMap.vue')
|
||||
const source = readFileSync(componentPath, 'utf-8')
|
||||
expect(source).toContain('basemaps.cartocdn.com/dark_all')
|
||||
expect(source).toContain('{s}.basemaps.cartocdn.com')
|
||||
})
|
||||
|
||||
it('requests client location first with maximumAge 0 so browser prompts', async () => {
|
||||
const componentPath = resolve(__dirname, '../../app/components/KestrelMap.vue')
|
||||
const source = readFileSync(componentPath, 'utf-8')
|
||||
expect(source).toContain('getCurrentPosition')
|
||||
expect(source).toContain('enableHighAccuracy: true')
|
||||
expect(source).toContain('maximumAge: 0')
|
||||
expect(source).toContain('createMap([latitude, longitude])')
|
||||
})
|
||||
|
||||
it('uses L.tileLayer for base display so tiles load from OSM', async () => {
|
||||
const componentPath = resolve(__dirname, '../../app/components/KestrelMap.vue')
|
||||
const source = readFileSync(componentPath, 'utf-8')
|
||||
expect(source).toContain('L.tileLayer(TILE_URL')
|
||||
expect(source).toContain('baseLayer.addTo(map)')
|
||||
})
|
||||
|
||||
it('sets Leaflet default marker icon path to avoid Vue Router intercept', async () => {
|
||||
const componentPath = resolve(__dirname, '../../app/components/KestrelMap.vue')
|
||||
const source = readFileSync(componentPath, 'utf-8')
|
||||
expect(source).toContain('Icon.Default.mergeOptions')
|
||||
expect(source).toContain('marker-icon.png')
|
||||
expect(source).toContain('marker-shadow.png')
|
||||
})
|
||||
|
||||
it('accepts pois and canEditPois props', async () => {
|
||||
const wrapper = await mountSuspended(KestrelMap, {
|
||||
props: {
|
||||
feeds: [],
|
||||
pois: [{ id: 'p1', lat: 37.7, lng: -122.4, label: 'P', icon_type: 'pin' }],
|
||||
canEditPois: false,
|
||||
},
|
||||
})
|
||||
expect(wrapper.props('pois')).toHaveLength(1)
|
||||
expect(wrapper.props('canEditPois')).toBe(false)
|
||||
})
|
||||
})
|
||||
59
test/nuxt/NavDrawer.spec.js
Normal file
59
test/nuxt/NavDrawer.spec.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mountSuspended, registerEndpoint } from '@nuxt/test-utils/runtime'
|
||||
import NavDrawer from '../../app/components/NavDrawer.vue'
|
||||
|
||||
const withAuth = () => {
|
||||
registerEndpoint('/api/me', () => ({ id: '1', identifier: 'user', role: 'member' }), { method: 'GET' })
|
||||
}
|
||||
|
||||
describe('NavDrawer', () => {
|
||||
it('renders navigation links with correct paths', async () => {
|
||||
withAuth()
|
||||
await mountSuspended(NavDrawer, {
|
||||
props: { modelValue: true },
|
||||
attachTo: document.body,
|
||||
})
|
||||
const links = document.body.querySelectorAll('aside nav a[href]')
|
||||
const hrefs = [...links].map(a => a.getAttribute('href'))
|
||||
expect(hrefs).toContain('/')
|
||||
expect(hrefs).toContain('/account')
|
||||
expect(hrefs).toContain('/cameras')
|
||||
expect(hrefs).toContain('/poi')
|
||||
expect(hrefs).toContain('/members')
|
||||
expect(hrefs).toContain('/settings')
|
||||
expect(links.length).toBeGreaterThanOrEqual(6)
|
||||
})
|
||||
|
||||
it('renders Map and Settings labels', async () => {
|
||||
withAuth()
|
||||
await mountSuspended(NavDrawer, {
|
||||
props: { modelValue: true },
|
||||
attachTo: document.body,
|
||||
})
|
||||
expect(document.body.textContent).toContain('Map')
|
||||
expect(document.body.textContent).toContain('Settings')
|
||||
expect(document.body.textContent).toContain('Navigation')
|
||||
})
|
||||
|
||||
it('emits update:modelValue when close is triggered', async () => {
|
||||
withAuth()
|
||||
const wrapper = await mountSuspended(NavDrawer, {
|
||||
props: { modelValue: true },
|
||||
attachTo: document.body,
|
||||
})
|
||||
expect(document.body.querySelector('aside button[aria-label="Close navigation"]')).toBeTruthy()
|
||||
await wrapper.vm.close()
|
||||
expect(wrapper.emitted('update:modelValue')).toEqual([[false]])
|
||||
})
|
||||
|
||||
it('applies active styling for current route', async () => {
|
||||
withAuth()
|
||||
await mountSuspended(NavDrawer, {
|
||||
props: { modelValue: true },
|
||||
attachTo: document.body,
|
||||
})
|
||||
const mapLink = document.body.querySelector('aside nav a[href="/"]')
|
||||
expect(mapLink).toBeTruthy()
|
||||
expect(mapLink.className).toMatch(/kestrel-accent|border-kestrel-accent/)
|
||||
})
|
||||
})
|
||||
14
test/nuxt/api.spec.js
Normal file
14
test/nuxt/api.spec.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { getValidFeeds } from '../../server/utils/feedUtils.js'
|
||||
|
||||
describe('API contract', () => {
|
||||
it('getValidFeeds returns array suitable for API response', () => {
|
||||
const raw = [
|
||||
{ id: '1', name: 'A', lat: 1, lng: 2 },
|
||||
{ id: '2', name: 'B', lat: 3, lng: 4 },
|
||||
]
|
||||
const out = getValidFeeds(raw)
|
||||
expect(Array.isArray(out)).toBe(true)
|
||||
expect(out).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
38
test/nuxt/auth-middleware.spec.js
Normal file
38
test/nuxt/auth-middleware.spec.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mountSuspended, registerEndpoint } from '@nuxt/test-utils/runtime'
|
||||
import Index from '../../app/pages/index.vue'
|
||||
import Login from '../../app/pages/login.vue'
|
||||
|
||||
describe('auth middleware', () => {
|
||||
it('allows /login without redirect when unauthenticated', async () => {
|
||||
registerEndpoint('/api/me', () => null, { method: 'GET' })
|
||||
const wrapper = await mountSuspended(Login)
|
||||
expect(wrapper.text()).toContain('Sign in')
|
||||
expect(wrapper.find('input[type="password"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('redirects to /login with redirect query when unauthenticated and visiting protected route', async () => {
|
||||
registerEndpoint('/api/me', () => null, { method: 'GET' })
|
||||
registerEndpoint('/api/cameras', () => ({ devices: [], liveSessions: [] }), { method: 'GET' })
|
||||
registerEndpoint('/api/pois', () => [], { method: 'GET' })
|
||||
await mountSuspended(Index)
|
||||
await new Promise(r => setTimeout(r, 200))
|
||||
const router = useRouter()
|
||||
await router.isReady()
|
||||
expect(router.currentRoute.value.path).toBe('/login')
|
||||
expect(router.currentRoute.value.query.redirect).toBe('/')
|
||||
})
|
||||
|
||||
it('401 handler redirects to login when API returns 401', async () => {
|
||||
registerEndpoint('/api/me', () => {
|
||||
throw createError({ statusCode: 401 })
|
||||
}, { method: 'GET' })
|
||||
registerEndpoint('/api/cameras', () => ({ devices: [], liveSessions: [] }), { method: 'GET' })
|
||||
registerEndpoint('/api/pois', () => [], { method: 'GET' })
|
||||
await mountSuspended(Index)
|
||||
await new Promise(r => setTimeout(r, 250))
|
||||
const router = useRouter()
|
||||
await router.isReady()
|
||||
expect(router.currentRoute.value.path).toBe('/login')
|
||||
})
|
||||
})
|
||||
44
test/nuxt/default-layout.spec.js
Normal file
44
test/nuxt/default-layout.spec.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mountSuspended, registerEndpoint } from '@nuxt/test-utils/runtime'
|
||||
import DefaultLayout from '../../app/layouts/default.vue'
|
||||
import NavDrawer from '../../app/components/NavDrawer.vue'
|
||||
|
||||
const withAuth = () => {
|
||||
registerEndpoint('/api/me', () => ({ id: '1', identifier: 'user', role: 'member' }), { method: 'GET' })
|
||||
}
|
||||
|
||||
describe('default layout', () => {
|
||||
it('renders KestrelOS header', async () => {
|
||||
withAuth()
|
||||
const wrapper = await mountSuspended(DefaultLayout)
|
||||
expect(wrapper.text()).toContain('KestrelOS')
|
||||
expect(wrapper.text()).toContain('Tactical Operations Center')
|
||||
})
|
||||
|
||||
it('renders drawer toggle with accessible label', async () => {
|
||||
withAuth()
|
||||
const wrapper = await mountSuspended(DefaultLayout)
|
||||
const toggle = wrapper.find('button[aria-label="Toggle navigation"]')
|
||||
expect(toggle.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders NavDrawer', async () => {
|
||||
withAuth()
|
||||
const wrapper = await mountSuspended(DefaultLayout)
|
||||
expect(wrapper.findComponent(NavDrawer).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('calls logout and navigates when Logout is clicked', async () => {
|
||||
withAuth()
|
||||
registerEndpoint('/api/auth/logout', () => null, { method: 'POST' })
|
||||
const wrapper = await mountSuspended(DefaultLayout)
|
||||
await new Promise(r => setTimeout(r, 100))
|
||||
const logoutBtn = wrapper.findAll('button').find(b => b.text().includes('Logout'))
|
||||
expect(logoutBtn).toBeDefined()
|
||||
await logoutBtn.trigger('click')
|
||||
await new Promise(r => setTimeout(r, 100))
|
||||
const router = useRouter()
|
||||
await router.isReady()
|
||||
expect(router.currentRoute.value.path).toBe('/')
|
||||
})
|
||||
})
|
||||
17
test/nuxt/index-page.spec.js
Normal file
17
test/nuxt/index-page.spec.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mountSuspended, registerEndpoint } from '@nuxt/test-utils/runtime'
|
||||
import Index from '../../app/pages/index.vue'
|
||||
|
||||
describe('index page', () => {
|
||||
it('renders map and uses cameras', async () => {
|
||||
registerEndpoint('/api/cameras', () => ({
|
||||
devices: [{ id: '1', name: 'F1', lat: 37.7, lng: -122.4, streamUrl: '', sourceType: 'mjpeg', device_type: 'feed' }],
|
||||
liveSessions: [],
|
||||
}))
|
||||
registerEndpoint('/api/pois', () => [])
|
||||
registerEndpoint('/api/me', () => null, { method: 'GET' })
|
||||
const wrapper = await mountSuspended(Index)
|
||||
await new Promise(r => setTimeout(r, 150))
|
||||
expect(wrapper.findComponent({ name: 'KestrelMap' }).exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
32
test/nuxt/login.spec.js
Normal file
32
test/nuxt/login.spec.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mountSuspended, registerEndpoint } from '@nuxt/test-utils/runtime'
|
||||
import Login from '../../app/pages/login.vue'
|
||||
|
||||
describe('login page', () => {
|
||||
it('renders sign in form (local auth always shown)', async () => {
|
||||
registerEndpoint('/api/auth/config', () => ({ oidc: { enabled: false, label: '' } }), { method: 'GET' })
|
||||
const wrapper = await mountSuspended(Login)
|
||||
await new Promise(r => setTimeout(r, 50))
|
||||
expect(wrapper.text()).toContain('Sign in')
|
||||
expect(wrapper.find('input[type="text"]').exists()).toBe(true)
|
||||
expect(wrapper.find('input[type="password"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('shows OIDC button when OIDC is enabled', async () => {
|
||||
registerEndpoint('/api/auth/config', () => ({ oidc: { enabled: true, label: 'Sign in with Authentik' } }), { method: 'GET' })
|
||||
await clearNuxtData('auth-config')
|
||||
const wrapper = await mountSuspended(Login)
|
||||
await new Promise(r => setTimeout(r, 150))
|
||||
expect(wrapper.text()).toContain('Sign in with Authentik')
|
||||
expect(wrapper.find('a[href*="/api/auth/oidc/authorize"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('shows both OIDC button and password form when OIDC is enabled', async () => {
|
||||
registerEndpoint('/api/auth/config', () => ({ oidc: { enabled: true, label: 'Sign in with OIDC' } }), { method: 'GET' })
|
||||
await clearNuxtData('auth-config')
|
||||
const wrapper = await mountSuspended(Login)
|
||||
await new Promise(r => setTimeout(r, 150))
|
||||
expect(wrapper.find('a[href*="/api/auth/oidc/authorize"]').exists()).toBe(true)
|
||||
expect(wrapper.find('input[type="password"]').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
19
test/nuxt/members-page.spec.js
Normal file
19
test/nuxt/members-page.spec.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mountSuspended, registerEndpoint } from '@nuxt/test-utils/runtime'
|
||||
import Members from '../../app/pages/members.vue'
|
||||
|
||||
describe('members page', () => {
|
||||
it('renders Members heading', async () => {
|
||||
registerEndpoint('/api/me', () => null, { method: 'GET' })
|
||||
registerEndpoint('/api/users', () => [])
|
||||
const wrapper = await mountSuspended(Members)
|
||||
expect(wrapper.text()).toContain('Members')
|
||||
})
|
||||
|
||||
it('shows sign in message when no user', async () => {
|
||||
registerEndpoint('/api/me', () => null, { method: 'GET' })
|
||||
registerEndpoint('/api/users', () => [])
|
||||
const wrapper = await mountSuspended(Members)
|
||||
expect(wrapper.text()).toMatch(/Sign in to view members/)
|
||||
})
|
||||
})
|
||||
19
test/nuxt/poi-page.spec.js
Normal file
19
test/nuxt/poi-page.spec.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mountSuspended, registerEndpoint } from '@nuxt/test-utils/runtime'
|
||||
import Poi from '../../app/pages/poi.vue'
|
||||
|
||||
describe('poi page', () => {
|
||||
it('renders POI placement heading', async () => {
|
||||
registerEndpoint('/api/pois', () => [])
|
||||
registerEndpoint('/api/me', () => null, { method: 'GET' })
|
||||
const wrapper = await mountSuspended(Poi)
|
||||
expect(wrapper.text()).toContain('POI placement')
|
||||
})
|
||||
|
||||
it('shows view-only message when cannot edit', async () => {
|
||||
registerEndpoint('/api/pois', () => [])
|
||||
registerEndpoint('/api/me', () => null, { method: 'GET' })
|
||||
const wrapper = await mountSuspended(Poi)
|
||||
expect(wrapper.text()).toMatch(/View-only|Sign in as admin/)
|
||||
})
|
||||
})
|
||||
28
test/nuxt/useCameras.spec.js
Normal file
28
test/nuxt/useCameras.spec.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mountSuspended, registerEndpoint } from '@nuxt/test-utils/runtime'
|
||||
import Index from '../../app/pages/index.vue'
|
||||
|
||||
describe('useCameras', () => {
|
||||
it('page uses cameras from API', async () => {
|
||||
registerEndpoint('/api/cameras', () => ({
|
||||
devices: [{ id: '1', name: 'Test', lat: 37.7, lng: -122.4, streamUrl: '', sourceType: 'mjpeg', device_type: 'feed' }],
|
||||
liveSessions: [],
|
||||
}))
|
||||
registerEndpoint('/api/pois', () => [])
|
||||
registerEndpoint('/api/me', () => null, { method: 'GET' })
|
||||
const wrapper = await mountSuspended(Index)
|
||||
await new Promise(r => setTimeout(r, 100))
|
||||
expect(wrapper.findComponent({ name: 'KestrelMap' }).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('handles API error and falls back to empty devices and liveSessions', async () => {
|
||||
registerEndpoint('/api/cameras', () => {
|
||||
throw new Error('network')
|
||||
})
|
||||
registerEndpoint('/api/pois', () => [])
|
||||
registerEndpoint('/api/me', () => null, { method: 'GET' })
|
||||
const wrapper = await mountSuspended(Index)
|
||||
await new Promise(r => setTimeout(r, 150))
|
||||
expect(wrapper.findComponent({ name: 'KestrelMap' }).exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
40
test/nuxt/useLiveSessions.spec.js
Normal file
40
test/nuxt/useLiveSessions.spec.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mountSuspended, registerEndpoint } from '@nuxt/test-utils/runtime'
|
||||
import { defineComponent, h } from 'vue'
|
||||
import { useLiveSessions } from '../../app/composables/useLiveSessions.js'
|
||||
|
||||
describe('useLiveSessions', () => {
|
||||
it('fetches sessions from API and returns sessions ref', async () => {
|
||||
registerEndpoint('/api/live', () => [
|
||||
{ id: 's1', label: 'Live 1', hasStream: true, lat: 37, lng: -122 },
|
||||
])
|
||||
registerEndpoint('/api/me', () => ({ id: '1', identifier: 'u', role: 'member' }), { method: 'GET' })
|
||||
const TestComponent = defineComponent({
|
||||
setup() {
|
||||
const { sessions } = useLiveSessions()
|
||||
return () => h('div', { 'data-sessions': JSON.stringify(sessions.value) })
|
||||
},
|
||||
})
|
||||
const wrapper = await mountSuspended(TestComponent)
|
||||
await new Promise(r => setTimeout(r, 100))
|
||||
expect(wrapper.find('[data-sessions]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('returns empty array when fetch fails', async () => {
|
||||
registerEndpoint('/api/live', () => {
|
||||
throw new Error('fetch failed')
|
||||
})
|
||||
registerEndpoint('/api/me', () => ({ id: '1', identifier: 'u', role: 'member' }), { method: 'GET' })
|
||||
const TestComponent = defineComponent({
|
||||
setup() {
|
||||
const { sessions } = useLiveSessions()
|
||||
return () => h('div', { 'data-sessions': JSON.stringify(sessions.value) })
|
||||
},
|
||||
})
|
||||
const wrapper = await mountSuspended(TestComponent)
|
||||
await new Promise(r => setTimeout(r, 150))
|
||||
const el = wrapper.find('[data-sessions]')
|
||||
expect(el.exists()).toBe(true)
|
||||
expect(JSON.parse(el.attributes('data-sessions'))).toEqual([])
|
||||
})
|
||||
})
|
||||
26
test/nuxt/useWebRTCFailureReason.spec.js
Normal file
26
test/nuxt/useWebRTCFailureReason.spec.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { registerEndpoint } from '@nuxt/test-utils/runtime'
|
||||
import { getWebRTCFailureReason } from '../../app/composables/useWebRTCFailureReason.js'
|
||||
|
||||
describe('useWebRTCFailureReason', () => {
|
||||
it('returns wrongHost null when server and client hostname match', async () => {
|
||||
registerEndpoint('/api/live/debug-request-host', () => ({ hostname: 'localhost' }))
|
||||
const result = await getWebRTCFailureReason()
|
||||
expect(result).toEqual({ wrongHost: null })
|
||||
})
|
||||
|
||||
it('returns wrongHost when server and client hostname differ', async () => {
|
||||
registerEndpoint('/api/live/debug-request-host', () => ({ hostname: 'server.example.com' }))
|
||||
const result = await getWebRTCFailureReason()
|
||||
expect(result.wrongHost).toBeDefined()
|
||||
expect(result.wrongHost?.serverHostname).toBe('server.example.com')
|
||||
})
|
||||
|
||||
it('returns wrongHost null when fetch fails', async () => {
|
||||
registerEndpoint('/api/live/debug-request-host', () => {
|
||||
throw new Error('network')
|
||||
})
|
||||
const result = await getWebRTCFailureReason()
|
||||
expect(result).toEqual({ wrongHost: null })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user