import { untrustedSql } from '@supabase/pg-meta'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { mockAnimationsApi } from 'jsdom-testing-mocks'
import { http, HttpResponse } from 'msw'
import { describe, expect, test, vi } from 'vitest'
import { RenameQueryModal } from './RenameQueryModal'
import type { SnippetWithContent } from '@/data/content/sql-folders-query'
import { customRender } from '@/tests/lib/custom-render'
import { addAPIMock, mswServer } from '@/tests/lib/msw'
mockAnimationsApi()
const createSnippet = (id: string, name: string): SnippetWithContent => ({
id,
name,
description: undefined,
status: 'saved',
visibility: 'user',
owner_id: 1,
project_id: 1,
favorite: false,
inserted_at: '2026-08-07T14:19:25.711Z',
updated_at: '2026-08-07T14:19:25.711Z',
folder_id: 'private-folder',
type: 'sql',
content: {
content_id: id,
schema_version: '1',
unchecked_sql: untrustedSql('select 1;'),
},
})
const SNIPPET_A = createSnippet('snippet-a', 'First query')
const SNIPPET_B = createSnippet('snippet-b', 'Second query')
const UNTITLED_A = createSnippet('untitled-a', 'Untitled query')
const UNTITLED_B = createSnippet('untitled-b', 'Untitled query')
/** The modal renders the AI title generator, which checks for an OpenAI key when self-hosted. */
const mockOpenAIKeyCheck = () =>
mswServer.use(
http.get('*/api/ai/sql/check-api-key', () => HttpResponse.json({ hasKey: false })),
http.get('*/platform/projects/default', () => HttpResponse.json({}, { status: 404 }))
)
const mockUpsert = () => {
const requests: Array<{ name: string; id: string }> = []
addAPIMock({
method: 'put',
path: '/platform/projects/:ref/content',
response: async ({ request }) => {
const body = (await request.json()) as { id: string; name: string }
requests.push({ id: body.id, name: body.name })
return HttpResponse.json({ ...SNIPPET_A, ...body })
},
})
return requests
}
const getNameInput = () => screen.getByLabelText('Name')
describe('RenameQueryModal', () => {
test('submits the new name for the selected snippet', async () => {
mockOpenAIKeyCheck()
const requests = mockUpsert()
const onComplete = vi.fn()
customRender(
)
await userEvent.clear(getNameInput())
await userEvent.type(getNameInput(), 'Renamed query')
fireEvent.click(screen.getByRole('button', { name: 'Rename query' }))
await waitFor(() => expect(onComplete).toHaveBeenCalledOnce())
expect(requests).toEqual([{ id: 'snippet-a', name: 'Renamed query' }])
})
test('shows the next snippet name after a successful rename (FE-4114)', async () => {
mockOpenAIKeyCheck()
mockUpsert()
const onComplete = vi.fn()
const { rerender } = customRender(
)
await userEvent.clear(getNameInput())
await userEvent.type(getNameInput(), 'Renamed query')
fireEvent.click(screen.getByRole('button', { name: 'Rename query' }))
await waitFor(() => expect(onComplete).toHaveBeenCalledOnce())
// The parent closes the modal, then reopens it for a different snippet
rerender(
)
rerender(
)
await waitFor(() => expect(getNameInput()).toHaveValue('Second query'))
// Nothing has changed yet, so there is nothing to submit
expect(screen.getByRole('button', { name: 'Rename query' })).toBeDisabled()
})
test('resets for the next snippet when both snippets share a name', async () => {
mockOpenAIKeyCheck()
const requests = mockUpsert()
const onComplete = vi.fn()
const { rerender } = customRender(
)
await userEvent.clear(getNameInput())
await userEvent.type(getNameInput(), 'Renamed query')
fireEvent.click(screen.getByRole('button', { name: 'Rename query' }))
await waitFor(() => expect(onComplete).toHaveBeenCalledOnce())
// The parent keeps the renamed snippet selected while closing the modal, then reopens it for
// a second snippet that still carries the same original name
rerender(
)
rerender(
)
await waitFor(() => expect(getNameInput()).toHaveValue('Untitled query'))
await userEvent.clear(getNameInput())
await userEvent.type(getNameInput(), 'Second renamed query')
fireEvent.click(screen.getByRole('button', { name: 'Rename query' }))
await waitFor(() => expect(onComplete).toHaveBeenCalledTimes(2))
expect(requests).toEqual([
{ id: 'untitled-a', name: 'Renamed query' },
{ id: 'untitled-b', name: 'Second renamed query' },
])
})
test('discards an abandoned edit when cancelled', async () => {
mockOpenAIKeyCheck()
const onCancel = vi.fn()
const { rerender } = customRender(
)
await userEvent.clear(getNameInput())
await userEvent.type(getNameInput(), 'Half-typed name')
await userEvent.click(screen.getByRole('button', { name: 'Cancel' }))
expect(onCancel).toHaveBeenCalledOnce()
rerender(
)
rerender(
)
await waitFor(() => expect(getNameInput()).toHaveValue('First query'))
})
})