11import { describe , expect , it } from "vitest" ;
22
33import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js" ;
4+ import { SuggestionMenu } from "./SuggestionMenu.js" ;
45
56/**
67 * @vitest -environment jsdom
@@ -28,22 +29,20 @@ function getSuggestionPluginState(editor: BlockNoteEditor) {
2829}
2930
3031/**
31- * Simulates typing a trigger character and dispatching the suggestion menu
32- * meta, mirroring what `handleTextInput` does when the user types "/".
32+ * Calls the `handleTextInput` prop of the SuggestionMenu plugin directly,
33+ * which mirrors what ProseMirror would do when the user types a character.
34+ * This allows us to test the `shouldTrigger` filtering path.
3335 */
34- function triggerSuggestionMenu ( editor : BlockNoteEditor , char : string ) {
36+ function simulateTextInput ( editor : BlockNoteEditor , char : string ) : boolean {
3537 const plugin = findSuggestionPlugin ( editor ) ;
3638 const view = editor . _tiptapEditor . view ;
37- // First insert the trigger character (like handleTextInput does)
38- view . dispatch ( view . state . tr . insertText ( char ) ) ;
39- // Then dispatch the meta to activate the suggestion menu
40- view . dispatch (
41- view . state . tr
42- . setMeta ( plugin , {
43- triggerCharacter : char ,
44- } )
45- . scrollIntoView ( ) ,
46- ) ;
39+ const from = view . state . selection . from ;
40+ const to = view . state . selection . to ;
41+ const handler = plugin . props . handleTextInput ;
42+ if ( ! handler ) {
43+ throw new Error ( "handleTextInput not found on SuggestionMenu plugin" ) ;
44+ }
45+ return ( handler as any ) ( view , from , to , char ) as boolean ;
4746}
4847
4948function createEditor ( ) {
@@ -56,6 +55,10 @@ function createEditor() {
5655describe ( "SuggestionMenu" , ( ) => {
5756 it ( "should open suggestion menu in a paragraph" , ( ) => {
5857 const editor = createEditor ( ) ;
58+ const sm = editor . getExtension ( SuggestionMenu ) ! ;
59+
60+ // Register "/" trigger character (no filter)
61+ sm . addSuggestionMenu ( { triggerCharacter : "/" } ) ;
5962
6063 editor . replaceBlocks ( editor . document , [
6164 {
@@ -70,8 +73,11 @@ describe("SuggestionMenu", () => {
7073 // Verify we start with no active suggestion menu
7174 expect ( getSuggestionPluginState ( editor ) ) . toBeUndefined ( ) ;
7275
73- // Trigger the suggestion menu
74- triggerSuggestionMenu ( editor , "/" ) ;
76+ // Simulate typing "/" — handleTextInput should trigger the menu
77+ const handled = simulateTextInput ( editor , "/" ) ;
78+
79+ // The input should be handled (menu opened)
80+ expect ( handled ) . toBe ( true ) ;
7581
7682 // Plugin state should now be defined (menu opened)
7783 const pluginState = getSuggestionPluginState ( editor ) ;
@@ -81,8 +87,17 @@ describe("SuggestionMenu", () => {
8187 editor . _tiptapEditor . destroy ( ) ;
8288 } ) ;
8389
84- it ( "should not open suggestion menu in table content" , ( ) => {
90+ it ( "should not open suggestion menu in table content when shouldTrigger returns false " , ( ) => {
8591 const editor = createEditor ( ) ;
92+ const sm = editor . getExtension ( SuggestionMenu ) ! ;
93+
94+ // Register "/" with a shouldTrigger filter that blocks table content.
95+ // This mirrors what BlockNoteDefaultUI does.
96+ sm . addSuggestionMenu ( {
97+ triggerCharacter : "/" ,
98+ shouldOpen : ( state ) =>
99+ ! state . selection . $from . parent . type . isInGroup ( "tableContent" ) ,
100+ } ) ;
86101
87102 editor . replaceBlocks ( editor . document , [
88103 {
@@ -105,21 +120,72 @@ describe("SuggestionMenu", () => {
105120 // Place cursor inside a table cell
106121 editor . setTextCursorPosition ( "table-0" , "start" ) ;
107122
108- // Verify the cursor is inside table content (the parent node is
109- // a tableParagraph which belongs to the "tableContent" group)
123+ // Verify the cursor is inside table content
110124 const $from = editor . _tiptapEditor . state . selection . $from ;
111125 expect ( $from . parent . type . isInGroup ( "tableContent" ) ) . toBe ( true ) ;
112126
113127 // Verify we start with no active suggestion menu
114128 expect ( getSuggestionPluginState ( editor ) ) . toBeUndefined ( ) ;
115129
116- // Attempt to trigger the suggestion menu
117- triggerSuggestionMenu ( editor , "/" ) ;
130+ // Simulate typing "/" — shouldTrigger should prevent the menu from opening
131+ const handled = simulateTextInput ( editor , "/" ) ;
132+
133+ // handleTextInput should return false (not handled) because
134+ // shouldTrigger rejected the context
135+ expect ( handled ) . toBe ( false ) ;
136+
137+ // Plugin state should remain undefined
138+ expect ( getSuggestionPluginState ( editor ) ) . toBeUndefined ( ) ;
139+
140+ editor . _tiptapEditor . destroy ( ) ;
141+ } ) ;
142+
143+ it ( "should still allow suggestion menus without shouldTrigger in table content" , ( ) => {
144+ const editor = createEditor ( ) ;
145+ const sm = editor . getExtension ( SuggestionMenu ) ! ;
146+
147+ // Register "@" WITHOUT a shouldTrigger filter — should still work in tables
148+ sm . addSuggestionMenu ( { triggerCharacter : "@" } ) ;
149+
150+ editor . replaceBlocks ( editor . document , [
151+ {
152+ id : "table-0" ,
153+ type : "table" ,
154+ content : {
155+ type : "tableContent" ,
156+ rows : [
157+ {
158+ cells : [ "Cell 1" , "Cell 2" , "Cell 3" ] ,
159+ } ,
160+ {
161+ cells : [ "Cell 4" , "Cell 5" , "Cell 6" ] ,
162+ } ,
163+ ] ,
164+ } ,
165+ } ,
166+ ] ) ;
167+
168+ // Place cursor inside a table cell
169+ editor . setTextCursorPosition ( "table-0" , "start" ) ;
170+
171+ // Verify the cursor is inside table content
172+ const $from = editor . _tiptapEditor . state . selection . $from ;
173+ expect ( $from . parent . type . isInGroup ( "tableContent" ) ) . toBe ( true ) ;
118174
119- // Plugin state should remain undefined because the cursor is inside
120- // table content, and the fix prevents the menu from activating there
175+ // Verify we start with no active suggestion menu
121176 expect ( getSuggestionPluginState ( editor ) ) . toBeUndefined ( ) ;
122177
178+ // Simulate typing "@" — no shouldTrigger filter, so it should still work
179+ const handled = simulateTextInput ( editor , "@" ) ;
180+
181+ // The input should be handled (menu opened)
182+ expect ( handled ) . toBe ( true ) ;
183+
184+ // Plugin state should now be defined
185+ const pluginState = getSuggestionPluginState ( editor ) ;
186+ expect ( pluginState ) . toBeDefined ( ) ;
187+ expect ( pluginState . triggerCharacter ) . toBe ( "@" ) ;
188+
123189 editor . _tiptapEditor . destroy ( ) ;
124190 } ) ;
125191} ) ;
0 commit comments