Skip to content

Commit 836cc29

Browse files
committed
fix tests and markdown handling
1 parent 7235603 commit 836cc29

10 files changed

Lines changed: 118 additions & 203 deletions

File tree

convex/_generated/api.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ import type * as gameMaps_triggers from "../gameMaps/triggers.js";
115115
import type * as gameMaps_types from "../gameMaps/types.js";
116116
import type * as gameMaps_validation from "../gameMaps/validation.js";
117117
import type * as http from "../http.js";
118+
import type * as notes_blocknote from "../notes/blocknote.js";
118119
import type * as notes_editorSpecs from "../notes/editorSpecs.js";
119120
import type * as notes_functions_captureNoteSnapshot from "../notes/functions/captureNoteSnapshot.js";
120121
import type * as notes_functions_createNote from "../notes/functions/createNote.js";
@@ -326,6 +327,7 @@ declare const fullApi: ApiFromModules<{
326327
"gameMaps/types": typeof gameMaps_types;
327328
"gameMaps/validation": typeof gameMaps_validation;
328329
http: typeof http;
330+
"notes/blocknote": typeof notes_blocknote;
329331
"notes/editorSpecs": typeof notes_editorSpecs;
330332
"notes/functions/captureNoteSnapshot": typeof notes_functions_captureNoteSnapshot;
331333
"notes/functions/createNote": typeof notes_functions_createNote;

convex/documentSnapshots/__tests__/rollbackEdgeCases.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,8 @@ describe('note rollback data integrity', () => {
182182
const originalBlocks = [
183183
{
184184
id: 'block-1',
185-
type: 'paragraph' as const,
186-
content: [{ type: 'text' as const, text: 'Original content' }],
185+
type: 'paragraph',
186+
content: [{ type: 'text', text: 'Original content' }],
187187
props: {},
188188
children: [],
189189
},
@@ -210,8 +210,8 @@ describe('note rollback data integrity', () => {
210210
const modifiedBlocks = [
211211
{
212212
id: 'block-1',
213-
type: 'paragraph' as const,
214-
content: [{ type: 'text' as const, text: 'Modified content' }],
213+
type: 'paragraph',
214+
content: [{ type: 'text', text: 'Modified content' }],
215215
props: {},
216216
children: [],
217217
},
@@ -303,8 +303,8 @@ describe('canvas rollback data integrity', () => {
303303
const modifiedBlocks = [
304304
{
305305
id: 'block-1',
306-
type: 'paragraph' as const,
307-
content: [{ type: 'text' as const, text: 'Modified canvas content' }],
306+
type: 'paragraph',
307+
content: [{ type: 'text', text: 'Modified canvas content' }],
308308
props: {},
309309
children: [],
310310
},

convex/documentSnapshots/__tests__/snapshot.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ describe('note snapshots capture Y.Doc state directly', () => {
3030
const blocks = [
3131
{
3232
id: 'block-1',
33-
type: 'paragraph' as const,
34-
content: [{ type: 'text' as const, text: 'Hello world' }],
33+
type: 'paragraph',
34+
content: [{ type: 'text', text: 'Hello world' }],
3535
props: {},
3636
children: [],
3737
},

convex/folders/__tests__/getFolderContentsForDownload.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ describe('getFolderContentsForDownload — collectItemsRecursively', () => {
6565
expect(item.type).toBe(SIDEBAR_ITEM_TYPES.notes)
6666
expect(item.name).toBe('Session Log.md')
6767
expect(item.path).toBe('Session Log.md')
68-
expect(item.type).toBe(SIDEBAR_ITEM_TYPES.notes)
6968
if (item.type === SIDEBAR_ITEM_TYPES.notes) {
7069
expect(item.content.length).toBe(1)
7170
}

convex/notes/blocknote.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type * as Y from 'yjs'
2+
import { BlockNoteEditor } from '@blocknote/core'
3+
import { blocksToYDoc as bnBlocksToYDoc, yDocToBlocks as bnYDocToBlocks } from '@blocknote/core/yjs'
4+
import { editorSchema } from './editorSpecs'
5+
import type { CustomBlock, CustomPartialBlock } from './editorSpecs'
6+
7+
function createHeadlessEditor() {
8+
return BlockNoteEditor.create({ schema: editorSchema, _headless: true })
9+
}
10+
11+
export function blocksToYDoc(blocks: Array<CustomPartialBlock>, fragment: string): Y.Doc {
12+
const editor = createHeadlessEditor()
13+
try {
14+
return bnBlocksToYDoc(editor, blocks, fragment)
15+
} finally {
16+
editor._tiptapEditor.destroy()
17+
}
18+
}
19+
20+
export function yDocToBlocks(doc: Y.Doc, fragment: string): Array<CustomBlock> {
21+
const editor = createHeadlessEditor()
22+
try {
23+
return bnYDocToBlocks(editor, doc, fragment)
24+
} finally {
25+
editor._tiptapEditor.destroy()
26+
}
27+
}

convex/notes/functions/createNote.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import * as Y from 'yjs'
2-
import { BlockNoteEditor } from '@blocknote/core'
3-
import { blocksToYDoc } from '@blocknote/core/yjs'
42
import { saveTopLevelBlocksForNote } from '../../blocks/functions/saveTopLevelBlocksForNote'
53
import {
64
findUniqueSidebarItemSlug,
@@ -10,7 +8,7 @@ import {
108
import { SIDEBAR_ITEM_LOCATION, SIDEBAR_ITEM_TYPES } from '../../sidebarItems/types/baseTypes'
119
import { createYjsDocument } from '../../yjsSync/functions/createYjsDocument'
1210
import { uint8ToArrayBuffer } from '../../yjsSync/functions/uint8ToArrayBuffer'
13-
import { editorSchema } from '../editorSpecs'
11+
import { blocksToYDoc } from '../blocknote'
1412
import { logEditHistory } from '../../editHistory/log'
1513
import { EDIT_HISTORY_ACTION } from '../../editHistory/types'
1614
import type { CampaignMutationCtx } from '../../functions'
@@ -81,17 +79,11 @@ export async function createNote(
8179
if (content && content.length > 0) {
8280
await saveTopLevelBlocksForNote(ctx, { noteId, content })
8381

84-
const editor = BlockNoteEditor.create({
85-
schema: editorSchema,
86-
_headless: true,
87-
})
88-
let doc: Y.Doc | undefined
82+
const doc = blocksToYDoc(content, 'document')
8983
try {
90-
doc = blocksToYDoc(editor, content, 'document')
9184
initialState = uint8ToArrayBuffer(Y.encodeStateAsUpdate(doc))
9285
} finally {
93-
doc?.destroy()
94-
editor._tiptapEditor.destroy()
86+
doc.destroy()
9587
}
9688
}
9789

convex/notes/mutations.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import { v } from 'convex/values'
2-
import { BlockNoteEditor } from '@blocknote/core'
3-
import { yDocToBlocks } from '@blocknote/core/yjs'
42
import { campaignMutation } from '../functions'
53
import { customBlockValidator } from '../blocks/schema'
64
import { saveTopLevelBlocksForNote } from '../blocks/functions/saveTopLevelBlocksForNote'
75
import { checkYjsWriteAccess } from '../yjsSync/functions/checkYjsAccess'
86
import { reconstructYDoc } from '../yjsSync/functions/reconstructYDoc'
97
import { createNote as createNoteFn } from './functions/createNote'
108
import { updateNote as updateNoteFn } from './functions/updateNote'
11-
import { editorSchema } from './editorSpecs'
9+
import { yDocToBlocks } from './blocknote'
1210
import type { Id } from '../_generated/dataModel'
1311

1412
export const updateNote = campaignMutation({
@@ -64,22 +62,15 @@ export const persistNoteBlocks = campaignMutation({
6462
await checkYjsWriteAccess(ctx, documentId)
6563

6664
const { doc } = await reconstructYDoc(ctx, documentId)
67-
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
68-
let editor: ReturnType<typeof BlockNoteEditor.create> | undefined
6965
try {
70-
editor = BlockNoteEditor.create({
71-
schema: editorSchema,
72-
_headless: true,
73-
})
74-
const blocks = yDocToBlocks(editor, doc, 'document')
66+
const blocks = yDocToBlocks(doc, 'document')
7567

7668
await saveTopLevelBlocksForNote(ctx, {
7769
noteId: documentId,
7870
content: blocks,
7971
})
8072
} finally {
8173
doc.destroy()
82-
editor?._tiptapEditor.destroy()
8374
}
8475

8576
return null
Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,26 @@
11
import * as Y from 'yjs'
2-
import { BlockNoteEditor } from '@blocknote/core'
3-
import { blocksToYDoc } from '@blocknote/core/yjs'
4-
import { editorSchema } from '../../notes/editorSpecs'
5-
import type { PartialBlock } from '@blocknote/core'
2+
import { blocksToYDoc } from '../../notes/blocknote'
3+
import type { CustomPartialBlock } from '../../notes/editorSpecs'
64

7-
type AnyPartialBlock = PartialBlock<any, any, any>
5+
export type TestInlineContent = {
6+
type: string
7+
text?: string
8+
styles?: Record<string, boolean | string>
9+
}
10+
11+
/**
12+
* Simplified block type for tests. BlockNote's `PartialBlock` has an optional
13+
* `type` discriminant which prevents TypeScript from narrowing per-variant in
14+
* object literals. This type captures the shape tests actually use while
15+
* keeping basic structural checking.
16+
*/
17+
export type TestBlock = {
18+
id?: string
19+
type: string
20+
props?: Record<string, unknown>
21+
content?: Array<TestInlineContent>
22+
children?: Array<TestBlock>
23+
}
824

925
export function makeYjsUpdate(): ArrayBuffer {
1026
const doc = new Y.Doc()
@@ -16,21 +32,15 @@ export function makeYjsUpdate(): ArrayBuffer {
1632
return ab as ArrayBuffer
1733
}
1834

19-
export function makeYjsUpdateWithBlocks(blocks: Array<AnyPartialBlock>): ArrayBuffer {
20-
const editor = BlockNoteEditor.create({
21-
schema: editorSchema,
22-
_headless: true,
23-
})
24-
let doc: Y.Doc | undefined
35+
export function makeYjsUpdateWithBlocks(blocks: Array<TestBlock>): ArrayBuffer {
36+
const doc = blocksToYDoc(blocks as Array<CustomPartialBlock>, 'document')
2537
try {
26-
doc = blocksToYDoc(editor, blocks, 'document')
2738
const update = Y.encodeStateAsUpdate(doc)
2839
return update.buffer.slice(
2940
update.byteOffset,
3041
update.byteOffset + update.byteLength,
3142
) as ArrayBuffer
3243
} finally {
33-
doc?.destroy()
34-
editor._tiptapEditor.destroy()
44+
doc.destroy()
3545
}
3646
}

src/features/editor/utils/__tests__/text-to-blocks.test.ts

Lines changed: 11 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
convertBlocksToMarkdown,
44
convertTextContentToBlocks,
55
convertTextToHTML,
6-
extractMarkdownLinks,
76
isMarkdownFile,
87
} from '~/features/editor/utils/text-to-blocks'
98
import type { CustomPartialBlock } from 'convex/notes/editorSpecs'
@@ -21,6 +20,12 @@ function getBlockText(block: CustomPartialBlock): string {
2120
.join('')
2221
}
2322

23+
type InlineContentItem = { type: string; text?: string; styles?: Record<string, boolean> }
24+
25+
function getInlineContent(block: CustomPartialBlock): Array<InlineContentItem> {
26+
return (block.content ?? []) as unknown as Array<InlineContentItem>
27+
}
28+
2429
interface ExpectedBlock {
2530
type: string
2631
text?: string
@@ -161,53 +166,6 @@ describe('isMarkdownFile', () => {
161166
})
162167
})
163168

164-
// ===========================================================================
165-
// extractMarkdownLinks
166-
// ===========================================================================
167-
168-
describe('extractMarkdownLinks', () => {
169-
it('replaces a regular markdown link with a placeholder', () => {
170-
const { text, placeholders } = extractMarkdownLinks('[text](url)')
171-
expect(placeholders.size).toBe(1)
172-
expect(text).not.toContain('[text](url)')
173-
// Placeholder should map back to original
174-
const [, original] = [...placeholders.entries()][0]
175-
expect(original).toBe('[text](url)')
176-
})
177-
178-
it('replaces an image link with a placeholder', () => {
179-
const { text, placeholders } = extractMarkdownLinks('![alt](url)')
180-
expect(placeholders.size).toBe(1)
181-
expect(text).not.toContain('![alt](url)')
182-
const [, original] = [...placeholders.entries()][0]
183-
expect(original).toBe('![alt](url)')
184-
})
185-
186-
it('replaces multiple links on one line', () => {
187-
const { placeholders } = extractMarkdownLinks('[a](b) and [c](d)')
188-
expect(placeholders.size).toBe(2)
189-
})
190-
191-
it('does not replace links inside code blocks', () => {
192-
const input = '```\n[text](url)\n```'
193-
const { text, placeholders } = extractMarkdownLinks(input)
194-
expect(placeholders.size).toBe(0)
195-
expect(text).toBe(input)
196-
})
197-
198-
it('does not modify lines without links', () => {
199-
const { text, placeholders } = extractMarkdownLinks('plain text')
200-
expect(placeholders.size).toBe(0)
201-
expect(text).toBe('plain text')
202-
})
203-
204-
it('preserves wiki links [[name]]', () => {
205-
const { text, placeholders } = extractMarkdownLinks('[[some note]]')
206-
expect(placeholders.size).toBe(0)
207-
expect(text).toBe('[[some note]]')
208-
})
209-
})
210-
211169
// ===========================================================================
212170
// convertBlocksToMarkdown (export path)
213171
// ===========================================================================
@@ -399,12 +357,7 @@ describe('convertTextContentToBlocks - markdown', () => {
399357
const blocks = md('**bold text**')
400358
const para = blocks.find((b) => b.type === 'paragraph')
401359
expect(para).toBeDefined()
402-
const content = para!.content as unknown as Array<{
403-
type: string
404-
text?: string
405-
styles?: Record<string, boolean>
406-
}>
407-
const boldItem = content.find((ic) => ic.styles?.bold)
360+
const boldItem = getInlineContent(para!).find((ic) => ic.styles?.bold)
408361
expect(boldItem).toBeDefined()
409362
expect(boldItem!.text).toBe('bold text')
410363
})
@@ -413,12 +366,7 @@ describe('convertTextContentToBlocks - markdown', () => {
413366
const blocks = md('*italic text*')
414367
const para = blocks.find((b) => b.type === 'paragraph')
415368
expect(para).toBeDefined()
416-
const content = para!.content as unknown as Array<{
417-
type: string
418-
text?: string
419-
styles?: Record<string, boolean>
420-
}>
421-
const italicItem = content.find((ic) => ic.styles?.italic)
369+
const italicItem = getInlineContent(para!).find((ic) => ic.styles?.italic)
422370
expect(italicItem).toBeDefined()
423371
expect(italicItem!.text).toBe('italic text')
424372
})
@@ -427,25 +375,15 @@ describe('convertTextContentToBlocks - markdown', () => {
427375
const blocks = md('~~struck~~')
428376
const para = blocks.find((b) => b.type === 'paragraph')
429377
expect(para).toBeDefined()
430-
const content = para!.content as unknown as Array<{
431-
type: string
432-
text?: string
433-
styles?: Record<string, boolean>
434-
}>
435-
const struckItem = content.find((ic) => ic.styles?.strike)
378+
const struckItem = getInlineContent(para!).find((ic) => ic.styles?.strike)
436379
expect(struckItem).toBeDefined()
437380
})
438381

439382
it('parses inline code', () => {
440383
const blocks = md('some `inline code` here')
441384
const para = blocks.find((b) => b.type === 'paragraph')
442385
expect(para).toBeDefined()
443-
const content = para!.content as unknown as Array<{
444-
type: string
445-
text?: string
446-
styles?: Record<string, boolean>
447-
}>
448-
const codeItem = content.find((ic) => ic.styles?.code)
386+
const codeItem = getInlineContent(para!).find((ic) => ic.styles?.code)
449387
expect(codeItem).toBeDefined()
450388
expect(codeItem!.text).toBe('inline code')
451389
})
@@ -512,7 +450,7 @@ describe('convertTextContentToBlocks - plain text', () => {
512450

513451
it('preserves empty lines as empty paragraphs', () => {
514452
const blocks = txt('A\n\nB')
515-
// Should have at least 3 blocks: A, empty, B (or A, B with spacing)
453+
// At least 2 blocks: A and B (parser may also insert an empty paragraph between them)
516454
expect(blocks.length).toBeGreaterThanOrEqual(2)
517455
const textsWithContent = blocks.filter((b) => getBlockText(b) !== '')
518456
expect(textsWithContent.length).toBe(2)

0 commit comments

Comments
 (0)