Files
supabase/apps/studio/components/interfaces/SQLEditor/useAddDefinitions.test.ts
Joshen Lim 7d28bcc26b joshenlim/fe 4204 notebooks intellisense toggle (#49300)
## Context

Adds an intellisense toggle for explorer notebooks similar to SQL editor
+ have QueryEditor render definitions via `useAddDefinition`
<img width="259" height="162" alt="image"
src="https://github.com/user-attachments/assets/278fdabd-1a24-4769-972e-1bce29060463"
/>

So intellisense will be running in the QueryEditor if intellisense is
enabled + source selected is database, otherwise will not run.

<img width="982" height="411" alt="image"
src="https://github.com/user-attachments/assets/19497aa0-36fc-49ab-853d-cb938b5b18e7"
/>


Also updated `useAddDefinition` logic to flush the table columns +
functions cache in react query
- For context in the past we had users run into browser performance
issues when definitions were loaded if their database is really big
- Hence why we originally added this intellisense toggle
- But we previously also required users to refresh the browser after
disabling intellisense, as a manual way to flush the cache
- So this change should remove the need to refresh the browser after
disabling intellisense

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added PostgreSQL IntelliSense with definitions, formatting, and code
completions in SQL editors.
* Added a notebook option to enable or disable IntelliSense, with the
preference saved between sessions.
  * Improved the notebook’s empty-state appearance.

* **Bug Fixes**
* Improved IntelliSense cleanup and prevented duplicate registrations
when disabled.
* Improved query execution state handling while background IntelliSense
data loads.

* **Tests**
* Added coverage for shared registration, cleanup, preference
persistence, and IntelliSense-related query handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-21 01:47:39 +08:00

152 lines
5.4 KiB
TypeScript

import { QueryClient } from '@tanstack/react-query'
import { waitFor } from '@testing-library/react'
import { HttpResponse } from 'msw'
import { describe, expect, it, vi } from 'vitest'
import { acquireSharedRegistration, useAddDefinitions } from './useAddDefinitions'
import { customRenderHook } from '@/tests/lib/custom-render'
import { addAPIMock } from '@/tests/lib/msw'
import { setupSqlEditorMocks } from '@/tests/lib/sql-editor-test-utils'
vi.mock('@/components/ui/CodeEditor/Providers/PgSQLCompletionProvider', () => ({
default: vi.fn((_monaco: unknown, pgInfoRef: unknown) => ({ __pgInfoRef: pgInfoRef })),
}))
vi.mock('@/components/ui/CodeEditor/Providers/PgSQLSignatureHelpProvider', () => ({
default: vi.fn((_monaco: unknown, pgInfoRef: unknown) => ({ __pgInfoRef: pgInfoRef })),
}))
describe('acquireSharedRegistration', () => {
it('only registers once for multiple concurrent callers sharing a key', () => {
const register = vi.fn(() => ({ dispose: vi.fn() }))
acquireSharedRegistration('test-key-1', register)
acquireSharedRegistration('test-key-1', register)
acquireSharedRegistration('test-key-1', register)
expect(register).toHaveBeenCalledTimes(1)
})
it('registers independently per key', () => {
const register = vi.fn(() => ({ dispose: vi.fn() }))
acquireSharedRegistration('test-key-2a', register)
acquireSharedRegistration('test-key-2b', register)
expect(register).toHaveBeenCalledTimes(2)
})
it('does not dispose while other callers are still holding the registration', () => {
const dispose = vi.fn()
const register = vi.fn(() => ({ dispose }))
const releaseA = acquireSharedRegistration('test-key-3', register)
acquireSharedRegistration('test-key-3', register)
releaseA()
expect(dispose).not.toHaveBeenCalled()
})
it('disposes once the last caller releases', () => {
const dispose = vi.fn()
const register = vi.fn(() => ({ dispose }))
const releaseA = acquireSharedRegistration('test-key-4', register)
const releaseB = acquireSharedRegistration('test-key-4', register)
releaseA()
releaseB()
expect(dispose).toHaveBeenCalledTimes(1)
})
it('registers again after a full release cycle', () => {
const register = vi.fn(() => ({ dispose: vi.fn() }))
const release = acquireSharedRegistration('test-key-5', register)
release()
acquireSharedRegistration('test-key-5', register)
expect(register).toHaveBeenCalledTimes(2)
})
it('is safe to release more times than acquired', () => {
const dispose = vi.fn()
const register = vi.fn(() => ({ dispose }))
const release = acquireSharedRegistration('test-key-6', register)
release()
expect(() => release()).not.toThrow()
expect(dispose).toHaveBeenCalledTimes(1)
})
})
describe('useAddDefinitions', () => {
const createMonaco = () =>
({
languages: {
registerCompletionItemProvider: vi.fn(() => ({ dispose: vi.fn() })),
registerSignatureHelpProvider: vi.fn(() => ({ dispose: vi.fn() })),
registerDocumentFormattingEditProvider: vi.fn(() => ({ dispose: vi.fn() })),
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any
it('keeps the registered provider reading fresh data after the registering editor unmounts, as long as a sibling editor is still active', async () => {
const getPgsqlCompletionProvider = (
await import('@/components/ui/CodeEditor/Providers/PgSQLCompletionProvider')
).default as unknown as ReturnType<typeof vi.fn>
let keywordWords = ['select']
setupSqlEditorMocks()
addAPIMock({
method: 'post',
path: '/platform/pg-meta/:ref/query',
response: ({ request }) => {
const key = new URL(request.url).searchParams.get('key')
if (key === 'keywords') return HttpResponse.json(keywordWords.map((word) => ({ word })))
return HttpResponse.json([])
},
})
const monaco = createMonaco()
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
const renderOptions = { queryClient }
// Two sibling notebook cells' editors, both mounted at once, sharing the same cache.
const editor1 = customRenderHook(
() => useAddDefinitions('', monaco, { enabled: true }),
renderOptions
)
const editor2 = customRenderHook(
() => useAddDefinitions('', monaco, { enabled: true }),
renderOptions
)
await waitFor(() =>
expect(monaco.languages.registerCompletionItemProvider).toHaveBeenCalledTimes(1)
)
// The first (and only, thanks to ref-counted registration) call registered the provider —
// capture the `pgInfoRef` it reads from.
const pgInfoRef = getPgsqlCompletionProvider.mock.calls[0][1] as {
current: { keywords: string[] }
}
await waitFor(() => expect(pgInfoRef.current.keywords).toEqual(['select']))
// Editor 1 — the one whose render happened to trigger the registration — closes. Editor 2
// is still open, so the provider must stay registered and stay current.
editor1.unmount()
expect(monaco.languages.registerCompletionItemProvider).toHaveBeenCalledTimes(1)
keywordWords = ['select', 'insert']
queryClient.invalidateQueries({ queryKey: ['projects', 'default', 'keywords'] })
editor2.rerender()
await waitFor(() => expect(pgInfoRef.current.keywords).toEqual(['select', 'insert']))
editor2.unmount()
})
})