mirror of
https://github.com/supabase/supabase.git
synced 2026-09-10 20:10:31 +08:00
## What kind of change does this PR introduce?
Bug fix.
## What is the current behavior?
With a snippet already open in the SQL Editor, clicking the `+` button
and typing in the new tab made it look like the previously open tab was
being taken over: the tab bar showed the *old* snippet's name while the
editor showed the newly typed content. With several tabs open, the
rightmost one appeared to be the one taken over.
`useSqlEditorTabsCleanup` prunes any `sql-*` tab whose snippet is absent
from the server-fetched snippet list, so tabs for snippets deleted in
another session don't linger. But a snippet created by typing in a new
tab exists only in the local store until its first save lands, so it is
legitimately absent from that list — and the same keystroke that creates
it calls `setSql({ shouldInvalidate: true })`, invalidating the snippet
lists and triggering a refetch that pruned the tab that had just been
opened.
`removeTab` then reassigns `activeTab` to a neighbor, so the tab bar
fell back to whichever tab was open before, while the URL, sidebar, and
editor content all stayed correctly on the new snippet — none of them
read from the tabs store.
Confirmed from a user's persisted tab state, which showed a single tab
in `openTabs` pointing at the previous snippet while the URL pointed at
the new one.
No snippet content was ever lost — this was tab state only.
## What is the new behavior?
Tabs (and recent items) whose snippet is present in the local store with
a never-persisted status (`new`, `new_saving`, `new_save_failed`) are
preserved by the cleanup pass. Genuine stale-tab pruning for snippets
removed outside the session is unaffected.
## Additional context
The regression test in `Tabs.utils.test.tsx` fails without the fix
(`expected undefined to be defined`) and passes with it. The
pre-existing pruning tests still pass, confirming legitimate cleanup
still works.
Also adds two tests covering adjacent invariants that were verified
while narrowing this down: snippet content isolation between tabs, and
unique snippet id generation across `/sql/new` navigations.
Verified: 343 tests pass across `components/layouts/Tabs` and
`components/interfaces/SQLEditor`; typecheck clean; ESLint warning count
unchanged (ratchet safe); Prettier clean.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **Bug Fixes**
- Preserved newly created SQL editor tabs and recent items before their
first save.
- Prevented unsaved snippets from being removed during tab cleanup.
- Ensured editing a new tab does not overwrite content in existing tabs.
- Ensured successive new SQL tabs receive distinct identities.
- **Tests**
- Added regression coverage for snippet editing, route changes, and tab
cleanup behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
57 lines
2.0 KiB
TypeScript
57 lines
2.0 KiB
TypeScript
import { act, waitFor } from '@testing-library/react'
|
|
import { beforeEach, describe, expect, it } from 'vitest'
|
|
|
|
import { useSnippetEditor } from './useSnippetEditor'
|
|
import { sqlEditorState } from '@/state/sql-editor/sql-editor-state'
|
|
import { createMockProfileContext } from '@/tests/lib/profile-helpers'
|
|
import {
|
|
renderSqlEditorHook,
|
|
resetSqlEditorStores,
|
|
seedSnippet,
|
|
setupSqlEditorMocks,
|
|
} from '@/tests/lib/sql-editor-test-utils'
|
|
|
|
const PROFILE_CONTEXT = createMockProfileContext()
|
|
|
|
describe('useSnippetEditor', () => {
|
|
beforeEach(() => {
|
|
resetSqlEditorStores()
|
|
setupSqlEditorMocks()
|
|
})
|
|
|
|
it('writing in a newly opened tab does not overwrite an already-open tab', async () => {
|
|
seedSnippet({ id: 'existing-tab', sql: 'select existing;' })
|
|
|
|
// Mount the editor for the already-open tab, mirroring the keyed
|
|
// MonacoEditor mount for that snippet id, then unmount it the way opening
|
|
// a new tab would (the `key={id}` wrapper swaps to a brand new instance).
|
|
const first = renderSqlEditorHook(useSnippetEditor, {
|
|
initialProps: { id: 'existing-tab', snippetName: 'Existing tab' },
|
|
profileContext: PROFILE_CONTEXT,
|
|
})
|
|
await waitFor(() => expect(first.result.current.snippet).toBeDefined())
|
|
first.unmount()
|
|
|
|
// Mount a fresh instance for a brand new tab, as clicking "+" would.
|
|
const second = renderSqlEditorHook(useSnippetEditor, {
|
|
initialProps: { id: 'new-tab-id', snippetName: 'New query' },
|
|
profileContext: PROFILE_CONTEXT,
|
|
})
|
|
|
|
// `handleEditorChange` only creates the new snippet once the project has
|
|
// loaded, so retry the keystroke until that happens.
|
|
await waitFor(() => {
|
|
act(() => {
|
|
second.result.current.handleEditorChange('select typed content;')
|
|
})
|
|
expect(sqlEditorState.snippets['new-tab-id']?.snippet.content?.unchecked_sql).toContain(
|
|
'typed content'
|
|
)
|
|
})
|
|
|
|
expect(sqlEditorState.snippets['existing-tab'].snippet.content?.unchecked_sql).toContain(
|
|
'select existing'
|
|
)
|
|
})
|
|
})
|