11/** @vitest -environment jsdom */
22import { act } from 'react'
3+ import { Editor } from '@tiptap/core'
34import type { ReactNodeViewProps } from '@tiptap/react'
5+ import StarterKit from '@tiptap/starter-kit'
46import { createRoot , type Root } from 'react-dom/client'
57import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
68
@@ -26,23 +28,24 @@ vi.mock(
2628)
2729
2830import { ResizableImageView } from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image'
31+ import { MarkdownImage } from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image-schema'
2932
3033let host : HTMLDivElement
3134let root : Root
32- const editor = { isEditable : true , isDestroyed : false , commands : { focus : vi . fn ( ) } }
35+ let editor : Editor
3336
3437beforeEach ( ( ) => {
3538 Object . assign ( globalThis , { IS_REACT_ACT_ENVIRONMENT : true } )
3639 vi . clearAllMocks ( )
37- editor . isEditable = true
38- editor . isDestroyed = false
40+ editor = new Editor ( { extensions : [ StarterKit , MarkdownImage ] } )
3941 host = document . createElement ( 'div' )
4042 document . body . append ( host )
4143 root = createRoot ( host )
4244} )
4345
4446afterEach ( ( ) => {
4547 act ( ( ) => root . unmount ( ) )
48+ editor . destroy ( )
4649 host . remove ( )
4750} )
4851
@@ -61,22 +64,31 @@ function pointerEvent(
6164}
6265
6366function renderImage (
64- updateAttributes : ReturnType < typeof vi . fn > ,
65- dimensions : { width ?: string | null ; height ?: string | null } = { }
67+ onUpdate : ReturnType < typeof vi . fn > ,
68+ dimensions : { width ?: string | null ; height ?: string | null } = { } ,
69+ getPos : ReactNodeViewProps [ 'getPos' ] = ( ) => 0
6670) : HTMLButtonElement {
67- const props = {
68- node : {
69- attrs : {
70- src : '/image.png' ,
71- alt : '' ,
72- title : null ,
73- width : null ,
74- height : '100' ,
75- ...dimensions ,
76- href : null ,
71+ editor . commands . setContent ( {
72+ type : 'doc' ,
73+ content : [
74+ {
75+ type : 'image' ,
76+ attrs : {
77+ src : '/image.png' ,
78+ alt : '' ,
79+ title : null ,
80+ width : null ,
81+ height : '100' ,
82+ ...dimensions ,
83+ href : null ,
84+ } ,
7785 } ,
78- } ,
79- updateAttributes,
86+ ] ,
87+ } )
88+ editor . on ( 'update' , onUpdate )
89+ const props = {
90+ node : editor . state . doc . firstChild ,
91+ getPos,
8092 selected : true ,
8193 editor,
8294 } as unknown as ReactNodeViewProps
@@ -150,42 +162,56 @@ describe('ResizableImageView', () => {
150162 )
151163
152164 it ( 'commits one proportional width change and clears a stale explicit height' , ( ) => {
153- const updateAttributes = vi . fn ( )
154- const handle = renderImage ( updateAttributes )
165+ const onUpdate = vi . fn ( )
166+ const handle = renderImage ( onUpdate )
155167
156168 act ( ( ) => handle . dispatchEvent ( pointerEvent ( 'pointerdown' , { pointerId : 7 , clientX : 100 } ) ) )
157169 act ( ( ) => window . dispatchEvent ( pointerEvent ( 'pointermove' , { pointerId : 7 , clientX : 160 } ) ) )
158170 act ( ( ) => window . dispatchEvent ( pointerEvent ( 'pointerup' , { pointerId : 7 , clientX : 160 } ) ) )
159171
160- expect ( updateAttributes ) . toHaveBeenCalledOnce ( )
161- expect ( updateAttributes ) . toHaveBeenCalledWith ( { width : '260' , height : null } )
172+ expect ( onUpdate ) . toHaveBeenCalledOnce ( )
173+ expect ( editor . state . doc . firstChild ?. attrs ) . toMatchObject ( { width : '260' , height : null } )
162174 } )
163175
164176 it ( 'ignores unrelated pointers and cancels without mutating document attributes' , ( ) => {
165- const updateAttributes = vi . fn ( )
166- const handle = renderImage ( updateAttributes )
177+ const onUpdate = vi . fn ( )
178+ const handle = renderImage ( onUpdate )
167179
168180 act ( ( ) => handle . dispatchEvent ( pointerEvent ( 'pointerdown' , { pointerId : 7 , clientX : 100 } ) ) )
169181 act ( ( ) => window . dispatchEvent ( pointerEvent ( 'pointermove' , { pointerId : 8 , clientX : 180 } ) ) )
170182 act ( ( ) => window . dispatchEvent ( pointerEvent ( 'pointerup' , { pointerId : 8 , clientX : 180 } ) ) )
171183 act ( ( ) => window . dispatchEvent ( pointerEvent ( 'pointercancel' , { pointerId : 7 } ) ) )
172- expect ( updateAttributes ) . not . toHaveBeenCalled ( )
184+ expect ( onUpdate ) . not . toHaveBeenCalled ( )
173185
174186 act ( ( ) => handle . dispatchEvent ( pointerEvent ( 'pointerdown' , { pointerId : 9 , clientX : 100 } ) ) )
175187 act ( ( ) => window . dispatchEvent ( pointerEvent ( 'pointermove' , { pointerId : 9 , clientX : 140 } ) ) )
176188 act ( ( ) => window . dispatchEvent ( new Event ( 'blur' ) ) )
177- expect ( updateAttributes ) . not . toHaveBeenCalled ( )
189+ expect ( onUpdate ) . not . toHaveBeenCalled ( )
178190 } )
179191
180192 it ( 'does not commit a resize after live editing becomes unavailable' , ( ) => {
181- const updateAttributes = vi . fn ( )
182- const handle = renderImage ( updateAttributes )
193+ const onUpdate = vi . fn ( )
194+ const handle = renderImage ( onUpdate )
195+
196+ act ( ( ) => handle . dispatchEvent ( pointerEvent ( 'pointerdown' , { pointerId : 7 , clientX : 100 } ) ) )
197+ act ( ( ) => window . dispatchEvent ( pointerEvent ( 'pointermove' , { pointerId : 7 , clientX : 160 } ) ) )
198+ editor . setEditable ( false , false )
199+ act ( ( ) => window . dispatchEvent ( pointerEvent ( 'pointerup' , { pointerId : 7 , clientX : 160 } ) ) )
200+
201+ expect ( onUpdate ) . not . toHaveBeenCalled ( )
202+ } )
203+
204+ it ( 'does not commit a resize when the node view no longer has a position' , ( ) => {
205+ const onUpdate = vi . fn ( )
206+ const getPos = vi . fn < ReactNodeViewProps [ 'getPos' ] > ( ( ) => 0 )
207+ const handle = renderImage ( onUpdate , { } , getPos )
183208
184209 act ( ( ) => handle . dispatchEvent ( pointerEvent ( 'pointerdown' , { pointerId : 7 , clientX : 100 } ) ) )
185210 act ( ( ) => window . dispatchEvent ( pointerEvent ( 'pointermove' , { pointerId : 7 , clientX : 160 } ) ) )
186- editor . isEditable = false
211+ getPos . mockReturnValue ( undefined )
187212 act ( ( ) => window . dispatchEvent ( pointerEvent ( 'pointerup' , { pointerId : 7 , clientX : 160 } ) ) )
188213
189- expect ( updateAttributes ) . not . toHaveBeenCalled ( )
214+ expect ( onUpdate ) . not . toHaveBeenCalled ( )
215+ expect ( editor . state . doc . firstChild ?. attrs ) . toMatchObject ( { width : null , height : '100' } )
190216 } )
191217} )
0 commit comments