feat(graph): add Federation Graph page and enhance coverage
Adds a new Federation Graph page to display subgraphs and their schemas. Implements loading state and error handling. Enhances coverage reporting by including 'lcov' format for better insights into test coverage metrics.
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const mockSelectOrganization = vi.fn()
|
||||
|
||||
const mockOrganizations = [
|
||||
{ id: '1', name: 'Org 1', apiKeys: [{ name: 'key1' }, { name: 'key2' }] },
|
||||
{ id: '2', name: 'Org 2', apiKeys: [{ name: 'key3' }] },
|
||||
{ id: '3', name: 'Org 3', apiKeys: [] },
|
||||
]
|
||||
|
||||
// TODO: Add more comprehensive component tests once vitest module mocking issues are resolved
|
||||
// For now, the component is indirectly tested through useOrganizationSelector tests
|
||||
// and manual testing shows it works correctly
|
||||
|
||||
describe('OrganizationSwitcher - Single Organization', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
mockSelectOrganization.mockClear()
|
||||
|
||||
// Mock with single organization
|
||||
vi.mock('~/composables/useOrganizationSelector', () => ({
|
||||
useOrganizationSelector: () => ({
|
||||
organizations: ref([mockOrganizations[0]]),
|
||||
selectedOrganization: ref(mockOrganizations[0]),
|
||||
selectedOrgId: ref('1'),
|
||||
selectOrganization: mockSelectOrganization,
|
||||
}),
|
||||
}))
|
||||
})
|
||||
|
||||
it('should show a chip for single organization', async () => {
|
||||
const module = await import('../OrganizationSwitcher.vue')
|
||||
const wrapper = mount(module.default)
|
||||
|
||||
const html = wrapper.html()
|
||||
|
||||
// Should show chip
|
||||
expect(html).toContain('v-chip')
|
||||
expect(html).toContain('Org 1')
|
||||
|
||||
// Menu should not exist
|
||||
expect(html).not.toContain('v-menu')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,135 @@
|
||||
import { afterEach,beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick,ref } from 'vue'
|
||||
|
||||
const mockOrganizations = [
|
||||
{ id: '1', name: 'Org 1' },
|
||||
{ id: '2', name: 'Org 2' },
|
||||
{ id: '3', name: 'Org 3' },
|
||||
]
|
||||
|
||||
const mockResult = ref({
|
||||
organizations: mockOrganizations,
|
||||
})
|
||||
|
||||
// Mock Auth0
|
||||
vi.mock('@auth0/auth0-vue', () => ({
|
||||
useAuth0: () => ({
|
||||
isAuthenticated: ref(true),
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock the GraphQL query
|
||||
vi.mock('~/graphql/generated', () => ({
|
||||
useOrganizationsQuery: () => ({
|
||||
result: mockResult,
|
||||
loading: ref(false),
|
||||
error: ref(null),
|
||||
refetch: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('useOrganizationSelector', () => {
|
||||
let useOrganizationSelector: any
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clear localStorage before each test
|
||||
localStorage.clear()
|
||||
|
||||
// Reset modules to clear the shared selectedOrgId ref
|
||||
vi.resetModules()
|
||||
|
||||
// Re-import the composable to get fresh module state
|
||||
const module = await import('../useOrganizationSelector')
|
||||
useOrganizationSelector = module.useOrganizationSelector
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
it('should return organizations list', async () => {
|
||||
const { organizations } = useOrganizationSelector()
|
||||
await nextTick()
|
||||
|
||||
expect(organizations.value).toHaveLength(3)
|
||||
expect(organizations.value[0]).toEqual({ id: '1', name: 'Org 1' })
|
||||
})
|
||||
|
||||
it('should auto-select first organization if none selected and orgs available', async () => {
|
||||
const { selectedOrganization, organizations } = useOrganizationSelector()
|
||||
await nextTick()
|
||||
|
||||
// Should auto-select first org
|
||||
expect(selectedOrganization.value).toEqual(organizations.value[0])
|
||||
})
|
||||
|
||||
it('should select an organization by ID', async () => {
|
||||
const { selectedOrganization, selectOrganization, organizations } = useOrganizationSelector()
|
||||
await nextTick()
|
||||
|
||||
// Should start with first org auto-selected
|
||||
expect(selectedOrganization.value).toEqual(organizations.value[0])
|
||||
|
||||
// Select a different organization by ID
|
||||
selectOrganization('2')
|
||||
await nextTick()
|
||||
|
||||
expect(selectedOrganization.value).toEqual({ id: '2', name: 'Org 2' })
|
||||
})
|
||||
|
||||
it('should persist selected organization ID to localStorage', async () => {
|
||||
const { selectOrganization } = useOrganizationSelector()
|
||||
await nextTick()
|
||||
|
||||
// Auto-selection will have set it to '1', so select '2' to trigger the watch
|
||||
selectOrganization('2')
|
||||
await nextTick()
|
||||
|
||||
const stored = localStorage.getItem('selectedOrgId')
|
||||
expect(stored).toBe('2')
|
||||
})
|
||||
|
||||
it('should restore selected organization from localStorage', async () => {
|
||||
// Set up localStorage with a saved organization ID
|
||||
localStorage.setItem('selectedOrgId', '2')
|
||||
|
||||
const { selectedOrganization } = useOrganizationSelector()
|
||||
await nextTick()
|
||||
|
||||
expect(selectedOrganization.value).toEqual({ id: '2', name: 'Org 2' })
|
||||
})
|
||||
|
||||
it('should return selectedOrgId computed property', async () => {
|
||||
const { selectedOrgId, selectOrganization } = useOrganizationSelector()
|
||||
await nextTick()
|
||||
|
||||
// Auto-selected to first org
|
||||
expect(selectedOrgId.value).toBe('1')
|
||||
|
||||
selectOrganization('3')
|
||||
await nextTick()
|
||||
|
||||
expect(selectedOrgId.value).toBe('3')
|
||||
})
|
||||
|
||||
it('should handle selecting an organization that does not exist', async () => {
|
||||
const { selectedOrganization, selectOrganization } = useOrganizationSelector()
|
||||
await nextTick()
|
||||
|
||||
selectOrganization('999')
|
||||
await nextTick()
|
||||
|
||||
// selectedOrgId is set but selectedOrganization returns null
|
||||
expect(selectedOrganization.value).toBeNull()
|
||||
})
|
||||
|
||||
it('should prioritize localStorage over auto-selection', async () => {
|
||||
localStorage.setItem('selectedOrgId', '3')
|
||||
|
||||
const { selectedOrganization } = useOrganizationSelector()
|
||||
await nextTick()
|
||||
|
||||
// Should restore from localStorage, not auto-select first org
|
||||
expect(selectedOrganization.value).toEqual({ id: '3', name: 'Org 3' })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<div class="d-flex align-center mb-4">
|
||||
<v-btn
|
||||
icon="mdi-arrow-left"
|
||||
variant="text"
|
||||
@click="$router.back()"
|
||||
/>
|
||||
<h1 class="text-h4 ml-2">
|
||||
Federation Graph: {{ route.params.ref }}
|
||||
</h1>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="loading">
|
||||
<v-col cols="12" class="text-center py-12">
|
||||
<v-progress-circular indeterminate color="primary" size="64" />
|
||||
<p class="mt-4">
|
||||
Loading federation graph...
|
||||
</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-else-if="supergraphData">
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<v-card-title>Subgraphs in Federation</v-card-title>
|
||||
<v-card-text>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="subgraph in subgraphs"
|
||||
:key="subgraph.service"
|
||||
:title="subgraph.service"
|
||||
:subtitle="`URL: ${subgraph.url}`"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-cube-outline" />
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<span>Federation Schema (SDL)</span>
|
||||
<v-btn
|
||||
icon="mdi-content-copy"
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="copySupergraph"
|
||||
/>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<pre class="sdl-viewer"><code>{{ supergraphData.sdl }}</code></pre>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-else>
|
||||
<v-col cols="12" class="text-center py-12">
|
||||
<v-icon icon="mdi-alert-circle" size="64" class="mb-4 text-warning" />
|
||||
<p class="text-h6">
|
||||
No federation graph available
|
||||
</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-snackbar v-model="snackbar" :timeout="3000">
|
||||
{{ snackbarText }}
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSupergraphQuery } from '~/graphql/generated'
|
||||
|
||||
const route = useRoute()
|
||||
const loading = ref(true)
|
||||
const snackbar = ref(false)
|
||||
const snackbarText = ref('')
|
||||
|
||||
const { result, loading: queryLoading } = useSupergraphQuery(() => ({
|
||||
ref: route.params.ref as string,
|
||||
isAfter: null,
|
||||
}))
|
||||
|
||||
const supergraphData = computed(() => result.value?.supergraph)
|
||||
const subgraphs = computed(() => supergraphData.value?.subgraphs || [])
|
||||
|
||||
watch(queryLoading, (newLoading) => {
|
||||
loading.value = newLoading
|
||||
})
|
||||
|
||||
const copySupergraph = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(supergraphData.value?.sdl || '')
|
||||
snackbarText.value = 'Supergraph SDL copied to clipboard'
|
||||
snackbar.value = true
|
||||
} catch (_err) {
|
||||
snackbarText.value = 'Failed to copy'
|
||||
snackbar.value = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sdl-viewer {
|
||||
background: #f5f5f5;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
overflow: auto;
|
||||
font-family: Monaco, Menlo, 'Ubuntu Mono', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
max-height: 60vh;
|
||||
}
|
||||
</style>
|
||||
@@ -82,6 +82,7 @@
|
||||
prepend-icon="mdi-graph"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
@click="viewFederationGraph"
|
||||
>
|
||||
View Federation Graph
|
||||
</v-btn>
|
||||
@@ -159,6 +160,11 @@ const copyToClipboard = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const viewFederationGraph = () => {
|
||||
if (!schema.value?.ref) return
|
||||
navigateTo(`/graph/${schema.value.ref}`)
|
||||
}
|
||||
|
||||
// TODO: Fetch actual data from the GraphQL API
|
||||
onMounted(() => {
|
||||
// Mock data for now
|
||||
|
||||
Reference in New Issue
Block a user