Files
supabase/apps/docs/app/api/graphql/tests/searchDocs.test.ts
Charis e46ab9c1a2 refactor: reading markdown docs files (#37774)
* refactor: reading markdown docs files

Refactor how Markdown docs files are read:
- Reuses the same logic across search index generation & page generation
- Improves the indexed content for search:
  - Stops removing MDX components, which often contain useful
    information like Admonitions
  - Denormalizes Partials and CodeSamples for more complete content

This is a prerequisite step for implementing the "Copy docs as Markdown"
functionality.

Only touches regular guides for now, not federated ones.

* fix: tailwind build error (#37728)

We changed to default to ESM imports a while ago, which means local
builds are now breaking because the Tailwind uses a require. Changed to
CJS for Tailwind config file. (I have no idea how this has been working
on Vercel all this time.)

* style: prettier
2025-08-13 11:37:14 -04:00

242 lines
6.7 KiB
TypeScript

import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
import { Result } from '~/features/helpers.fn'
import { type OpenAIClientInterface } from '~/lib/openAi'
import { ApiError } from '../../utils'
import { POST } from '../route'
const contentEmbeddingMock = vi
.fn()
.mockImplementation(async () => Result.ok({ embedding: [0.1, 0.2, 0.3], tokenCount: 10 }))
const openAIMock: OpenAIClientInterface = {
createContentEmbedding: contentEmbeddingMock,
}
vi.mock(import('~/lib/openAi'), () => ({
openAI: () => openAIMock,
}))
const rpcSpy = vi.fn().mockImplementation((funcName, params) => {
if (funcName === 'search_content_hybrid') {
const limit = params?.max_result || 2
const mockResults = [
{
type: 'markdown',
page_title: 'Test Guide',
href: '/guides/test',
content: params?.include_full_content ? 'Test content' : null,
subsections: [
{ title: 'Introduction', content: 'Introduction content' },
{ title: 'Details', content: 'Details content' },
],
},
{
type: 'markdown',
page_title: 'Another Guide',
href: '/guides/another',
content: params?.include_full_content ? 'Another content' : null,
subsections: [{ title: 'Getting Started', content: 'Getting Started content' }],
},
{
type: 'reference',
page_title: 'Create a SSO provider',
href: 'https://supabase.com/docs/reference/api/v1-create-a-sso-provider',
content: params?.include_full_content ? 'Creates a new SSO provider for a project' : null,
metadata: {
title: 'Create a SSO provider',
subtitle: 'Management API Reference: Create a SSO provider',
},
},
]
return Promise.resolve({ data: mockResults.slice(0, limit), error: null })
}
return Promise.resolve({ data: [], error: null })
})
vi.mock('~/lib/supabase', () => ({
supabase: () => ({
rpc: rpcSpy,
}),
}))
describe('/api/graphql searchDocs', () => {
beforeAll(() => {
vi.spyOn(console, 'error').mockImplementation(() => {})
})
afterAll(() => {
vi.restoreAllMocks()
vi.doUnmock('~/lib/openAi')
vi.doUnmock('~/lib/supabase')
})
it('should return search results when given a valid query', async () => {
const searchQuery = `
query {
searchDocs(query: "authentication") {
nodes {
title
href
}
}
}
`
const request = new Request('http://localhost/api/graphql', {
method: 'POST',
body: JSON.stringify({ query: searchQuery }),
})
const response = await POST(request)
const json = await response.json()
expect(json.errors).toBeUndefined()
expect(json.data).toBeDefined()
expect(json.data.searchDocs).toBeDefined()
expect(json.data.searchDocs.nodes).toBeInstanceOf(Array)
expect(json.data.searchDocs.nodes).toHaveLength(3)
expect(json.data.searchDocs.nodes[0]).toMatchObject({
title: 'Test Guide',
href: '/guides/test',
})
})
it('should respect the limit parameter', async () => {
const searchQuery = `
query {
searchDocs(query: "database", limit: 1) {
nodes {
title
}
}
}
`
const request = new Request('http://localhost/api/graphql', {
method: 'POST',
body: JSON.stringify({ query: searchQuery }),
})
const response = await POST(request)
const json = await response.json()
expect(json.errors).toBeUndefined()
expect(json.data.searchDocs.nodes).toHaveLength(1)
expect(json.data.searchDocs.nodes[0].title).toBe('Test Guide')
expect(rpcSpy).toHaveBeenCalledWith(
'search_content_hybrid',
expect.objectContaining({
max_result: 1,
})
)
})
it('should include content field when requested', async () => {
const searchQuery = `
query {
searchDocs(query: "api") {
nodes {
title
content
}
}
}
`
const request = new Request('http://localhost/api/graphql', {
method: 'POST',
body: JSON.stringify({ query: searchQuery }),
})
const response = await POST(request)
const json = await response.json()
expect(json.errors).toBeUndefined()
expect(json.data.searchDocs.nodes[0].content).toBe('Test content')
expect(rpcSpy).toHaveBeenCalledWith(
'search_content_hybrid',
expect.objectContaining({
include_full_content: true,
})
)
})
it('should handle errors from embedding creation', async () => {
contentEmbeddingMock.mockImplementationOnce(() => {
return Result.error(new ApiError('Embedding generation failed'))
})
const searchQuery = `
query {
searchDocs(query: "failed query") {
nodes {
title
}
}
}
`
const request = new Request('http://localhost/api/graphql', {
method: 'POST',
body: JSON.stringify({ query: searchQuery }),
})
const response = await POST(request)
const json = await response.json()
expect(json.errors).toBeDefined()
expect(json.errors[0].message).toBe('Internal Server Error')
})
it('should require a query parameter', async () => {
const searchQuery = `
query {
searchDocs {
nodes {
title
}
}
}
`
const request = new Request('http://localhost/api/graphql', {
method: 'POST',
body: JSON.stringify({ query: searchQuery }),
})
const response = await POST(request)
const json = await response.json()
expect(json.errors).toBeDefined()
expect(json.errors[0].message).toContain('required')
})
it('should return Management API references with proper fields', async () => {
const searchQuery = `
query {
searchDocs(query: "SSO provider", limit: 3) {
nodes {
... on ManagementApiReference {
title
href
content
}
}
}
}
`
const request = new Request('http://localhost/api/graphql', {
method: 'POST',
body: JSON.stringify({ query: searchQuery }),
})
const response = await POST(request)
const json = await response.json()
expect(json.errors).toBeUndefined()
expect(json.data).toBeDefined()
expect(json.data.searchDocs).toBeDefined()
expect(json.data.searchDocs.nodes).toBeInstanceOf(Array)
expect(json.data.searchDocs.nodes).toHaveLength(3)
const managementApiNode = json.data.searchDocs.nodes[2]
expect(managementApiNode).toMatchObject({
title: 'Create a SSO provider',
href: 'https://supabase.com/docs/reference/api/v1-create-a-sso-provider',
content: 'Creates a new SSO provider for a project',
})
})
})