Skip to content

Commit f3806b9

Browse files
fix: normalize invalid saved ids in code and notes (#732)
1 parent ec0f1f7 commit f3806b9

12 files changed

+572
-19
lines changed
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
import { computed, reactive, ref } from 'vue'
3+
4+
globalThis.computed = computed
5+
globalThis.reactive = reactive
6+
globalThis.ref = ref
7+
8+
interface FolderNode {
9+
id: number
10+
children: FolderNode[]
11+
}
12+
13+
interface SetupOptions {
14+
displayedSnippetIds?: number[]
15+
folderId?: number
16+
folders?: FolderNode[]
17+
snippetId?: number
18+
tagId?: number
19+
tags?: Array<{ id: number, name: string }>
20+
}
21+
22+
async function setup(options: SetupOptions = {}) {
23+
vi.resetModules()
24+
25+
const state = reactive<{
26+
folderId?: number
27+
libraryFilter?: string
28+
snippetId?: number
29+
tagId?: number
30+
}>({
31+
folderId: options.folderId,
32+
snippetId: options.snippetId,
33+
tagId: options.tagId,
34+
})
35+
36+
const folders = ref<FolderNode[] | undefined>(
37+
options.folders ?? [
38+
{ id: 7, children: [] },
39+
{ id: 9, children: [] },
40+
],
41+
)
42+
const tags = ref(options.tags ?? [{ id: 1, name: 'work' }])
43+
const displayedSnippets = ref(
44+
(options.displayedSnippetIds ?? []).map(id => ({ id })),
45+
)
46+
47+
const getSnippets = vi.fn(async () => undefined)
48+
const selectFirstSnippet = vi.fn(() => {
49+
state.snippetId = displayedSnippets.value[0]?.id
50+
})
51+
52+
vi.doMock('../useApp', () => ({
53+
useApp: () => ({
54+
state,
55+
}),
56+
}))
57+
58+
vi.doMock('../useFolders', () => ({
59+
useFolders: () => ({
60+
folders,
61+
}),
62+
}))
63+
64+
vi.doMock('../useTags', () => ({
65+
useTags: () => ({
66+
tags,
67+
}),
68+
}))
69+
70+
vi.doMock('../useSnippets', () => ({
71+
useSnippets: () => ({
72+
displayedSnippets,
73+
getSnippets,
74+
selectFirstSnippet,
75+
}),
76+
}))
77+
78+
const { normalizeCodeSelectionState } = await import(
79+
'../useCodeSelectionNormalization'
80+
)
81+
82+
return {
83+
displayedSnippets,
84+
folders,
85+
getSnippets,
86+
normalizeCodeSelectionState,
87+
selectFirstSnippet,
88+
state,
89+
tags,
90+
}
91+
}
92+
93+
beforeEach(() => {
94+
vi.clearAllMocks()
95+
})
96+
97+
describe('normalizeCodeSelectionState', () => {
98+
it('clears stale tagId before requesting snippets', async () => {
99+
const context = await setup({
100+
folderId: 7,
101+
tagId: 999,
102+
})
103+
104+
await context.normalizeCodeSelectionState()
105+
106+
expect(context.state.tagId).toBeUndefined()
107+
expect(context.getSnippets).toHaveBeenCalledTimes(1)
108+
})
109+
110+
it('falls back to the first folder when saved folderId is missing', async () => {
111+
const context = await setup({
112+
folderId: 999,
113+
folders: [
114+
{ id: 7, children: [] },
115+
{ id: 9, children: [] },
116+
],
117+
})
118+
119+
await context.normalizeCodeSelectionState()
120+
121+
expect(context.state.folderId).toBe(7)
122+
expect(context.getSnippets).toHaveBeenCalledTimes(1)
123+
})
124+
125+
it('selects the first snippet when saved snippetId is absent from the list', async () => {
126+
const context = await setup({
127+
displayedSnippetIds: [1, 2],
128+
folderId: 7,
129+
snippetId: 42,
130+
})
131+
132+
await context.normalizeCodeSelectionState()
133+
134+
expect(context.selectFirstSnippet).toHaveBeenCalledTimes(1)
135+
expect(context.state.snippetId).toBe(1)
136+
})
137+
})
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
import { ref } from 'vue'
3+
4+
globalThis.ref = ref
5+
6+
async function setup() {
7+
vi.resetModules()
8+
9+
const callOrder: string[] = []
10+
const isCodeSpaceInitialized = ref(false)
11+
const getFolders = vi.fn(async () => {
12+
callOrder.push('getFolders')
13+
})
14+
const getSnippets = vi.fn(async () => {
15+
callOrder.push('getSnippets')
16+
})
17+
const getTags = vi.fn(async () => {
18+
callOrder.push('getTags')
19+
})
20+
const normalizeCodeSelectionState = vi.fn(async () => {
21+
callOrder.push('normalizeCodeSelectionState')
22+
})
23+
24+
vi.doMock('../useApp', () => ({
25+
useApp: () => ({
26+
isCodeSpaceInitialized,
27+
}),
28+
}))
29+
30+
vi.doMock('../useFolders', () => ({
31+
useFolders: () => ({
32+
getFolders,
33+
}),
34+
}))
35+
36+
vi.doMock('../useTags', () => ({
37+
useTags: () => ({
38+
getTags,
39+
}),
40+
}))
41+
42+
vi.doMock('../useSnippets', () => ({
43+
useSnippets: () => ({
44+
getSnippets,
45+
}),
46+
}))
47+
48+
vi.doMock('../useCodeSelectionNormalization', () => ({
49+
normalizeCodeSelectionState,
50+
}))
51+
52+
const { initCodeSpace } = await import('../useCodeSpaceInit')
53+
54+
return {
55+
callOrder,
56+
initCodeSpace,
57+
isCodeSpaceInitialized,
58+
normalizeCodeSelectionState,
59+
}
60+
}
61+
62+
beforeEach(() => {
63+
vi.clearAllMocks()
64+
})
65+
66+
describe('initCodeSpace', () => {
67+
it('loads folders and tags before normalizing selection state', async () => {
68+
const context = await setup()
69+
70+
await context.initCodeSpace()
71+
72+
expect(context.callOrder).toEqual([
73+
'getFolders',
74+
'getTags',
75+
'normalizeCodeSelectionState',
76+
])
77+
expect(context.isCodeSpaceInitialized.value).toBe(true)
78+
})
79+
})

src/renderer/composables/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './math-notebook'
22
export * from './spaces/notes'
33
export * from './useApp'
4+
export * from './useCodeSelectionNormalization'
45
export * from './useCodeSpaceInit'
56
export * from './useCopyToClipboard'
67
export * from './useDialog'
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
import { computed, reactive, ref } from 'vue'
3+
4+
globalThis.computed = computed
5+
globalThis.reactive = reactive
6+
globalThis.ref = ref
7+
8+
interface FolderNode {
9+
id: number
10+
children: FolderNode[]
11+
}
12+
13+
interface SetupOptions {
14+
displayedNoteIds?: number[]
15+
folderId?: number
16+
folders?: FolderNode[]
17+
noteId?: number
18+
tagId?: number
19+
tags?: Array<{ id: number, name: string }>
20+
}
21+
22+
async function setup(options: SetupOptions = {}) {
23+
vi.resetModules()
24+
25+
const notesState = reactive<{
26+
folderId?: number
27+
libraryFilter?: string
28+
noteId?: number
29+
tagId?: number
30+
}>({
31+
folderId: options.folderId,
32+
noteId: options.noteId,
33+
tagId: options.tagId,
34+
})
35+
36+
const folders = ref<FolderNode[] | undefined>(
37+
options.folders ?? [
38+
{ id: 11, children: [] },
39+
{ id: 12, children: [] },
40+
],
41+
)
42+
const tags = ref(options.tags ?? [{ id: 1, name: 'docs' }])
43+
const displayedNotes = ref(
44+
(options.displayedNoteIds ?? []).map(id => ({ id })),
45+
)
46+
47+
const getNotes = vi.fn(async () => undefined)
48+
const selectFirstNote = vi.fn(() => {
49+
notesState.noteId = displayedNotes.value[0]?.id
50+
})
51+
52+
vi.doMock('../useNotesApp', () => ({
53+
useNotesApp: () => ({
54+
notesState,
55+
}),
56+
}))
57+
58+
vi.doMock('../useNoteFolders', () => ({
59+
useNoteFolders: () => ({
60+
folders,
61+
}),
62+
}))
63+
64+
vi.doMock('../useNoteTags', () => ({
65+
useNoteTags: () => ({
66+
tags,
67+
}),
68+
}))
69+
70+
vi.doMock('../useNotes', () => ({
71+
useNotes: () => ({
72+
getNotes,
73+
selectFirstNote,
74+
}),
75+
}))
76+
77+
vi.doMock('../useNoteSearch', () => ({
78+
useNoteSearch: () => ({
79+
displayedNotes,
80+
}),
81+
}))
82+
83+
const { normalizeNotesSelectionState } = await import(
84+
'../useNotesSelectionNormalization'
85+
)
86+
87+
return {
88+
getNotes,
89+
normalizeNotesSelectionState,
90+
notesState,
91+
selectFirstNote,
92+
}
93+
}
94+
95+
beforeEach(() => {
96+
vi.clearAllMocks()
97+
})
98+
99+
describe('normalizeNotesSelectionState', () => {
100+
it('clears stale tagId before requesting notes', async () => {
101+
const context = await setup({
102+
folderId: 11,
103+
tagId: 999,
104+
})
105+
106+
await context.normalizeNotesSelectionState()
107+
108+
expect(context.notesState.tagId).toBeUndefined()
109+
expect(context.getNotes).toHaveBeenCalledTimes(1)
110+
})
111+
112+
it('falls back to the first folder when saved notes folderId is missing', async () => {
113+
const context = await setup({
114+
folderId: 999,
115+
folders: [
116+
{ id: 11, children: [] },
117+
{ id: 12, children: [] },
118+
],
119+
})
120+
121+
await context.normalizeNotesSelectionState()
122+
123+
expect(context.notesState.folderId).toBe(11)
124+
expect(context.getNotes).toHaveBeenCalledTimes(1)
125+
})
126+
127+
it('selects the first note when saved noteId is absent from the list', async () => {
128+
const context = await setup({
129+
displayedNoteIds: [3, 4],
130+
folderId: 11,
131+
noteId: 99,
132+
})
133+
134+
await context.normalizeNotesSelectionState()
135+
136+
expect(context.selectFirstNote).toHaveBeenCalledTimes(1)
137+
expect(context.notesState.noteId).toBe(3)
138+
})
139+
})

0 commit comments

Comments
 (0)