11import { describe , it , expect , vi , beforeEach , afterEach } from "vitest" ;
22import { handleMermaidPreview , handleMermaidSave } from "../src/handlers.js" ;
33import { getPreviewDir , getDiagramFilePath } from "../src/file-utils.js" ;
4- import { mkdir , readdir , unlink , access , mkdtemp } from "fs/promises" ;
5- import { join } from "path" ;
4+ import { readdir , unlink , access } from "fs/promises" ;
65import { execFile } from "child_process" ;
7- import { tmpdir } from "os " ;
6+ import { setupTestEnvWithPreview , restoreTestEnv } from "./helpers/env-helpers.js " ;
87
98// Mock execFile to avoid actually running mmdc and create fake output files
109vi . mock ( "child_process" , ( ) => ( {
1110 execFile : vi . fn ( ( _file : string , args : string [ ] , callback : Function ) => {
12- // Find the output file from args array
1311 const outputIndex = args . indexOf ( "-o" ) ;
1412 if ( outputIndex !== - 1 && outputIndex + 1 < args . length ) {
1513 const outputFile = args [ outputIndex + 1 ] ;
16- // Create a fake output file synchronously
1714 const fs = require ( "fs" ) ;
1815 const path = require ( "path" ) ;
1916 const dir = path . dirname ( outputFile ) ;
2017
21- // Ensure directory exists
2218 if ( ! fs . existsSync ( dir ) ) {
2319 fs . mkdirSync ( dir , { recursive : true } ) ;
2420 }
2521
26- // Write fake content based on file extension
2722 const ext = path . extname ( outputFile ) ;
2823 if ( ext === ".svg" ) {
2924 fs . writeFileSync ( outputFile , "<svg>test</svg>" , "utf-8" ) ;
3025 } else if ( ext === ".png" ) {
31- // Write minimal PNG header
3226 fs . writeFileSync ( outputFile , Buffer . from ( [ 137 , 80 , 78 , 71 , 13 , 10 , 26 , 10 ] ) ) ;
3327 } else if ( ext === ".pdf" ) {
34- // Write minimal PDF header
3528 fs . writeFileSync ( outputFile , "%PDF-1.4\n" , "utf-8" ) ;
3629 } else {
3730 fs . writeFileSync ( outputFile , "test" , "utf-8" ) ;
@@ -44,7 +37,6 @@ vi.mock("child_process", () => ({
4437 } ) ,
4538} ) ) ;
4639
47- // Mock live server functions
4840vi . mock ( "../src/live-server.js" , ( ) => ( {
4941 ensureLiveServer : vi . fn ( async ( ) => 3737 ) ,
5042 addLiveDiagram : vi . fn ( async ( ) => { } ) ,
@@ -54,53 +46,24 @@ vi.mock("../src/live-server.js", () => ({
5446describe ( "handleMermaidPreview" , ( ) => {
5547 const testPreviewId = "test-preview" ;
5648 let testDir : string ;
57- let originalHome : string | undefined ;
58- let originalXdgConfig : string | undefined ;
5949
6050 beforeEach ( async ( ) => {
61- // Override config dirs to use a temp HOME/XDG path for isolation
62- originalHome = process . env . HOME ;
63- originalXdgConfig = process . env . XDG_CONFIG_HOME ;
64- const tempHome = await mkdtemp ( join ( tmpdir ( ) , "claude-mermaid-test-home-" ) ) ;
65- const tempConfigDir = join ( tempHome , ".config" ) ;
66- process . env . HOME = tempHome ;
67- process . env . XDG_CONFIG_HOME = tempConfigDir ;
68-
69- await mkdir ( tempConfigDir , { recursive : true } ) ;
70- testDir = getPreviewDir ( testPreviewId ) ;
71- await mkdir ( testDir , { recursive : true } ) ;
51+ testDir = await setupTestEnvWithPreview ( testPreviewId ) ;
7252 } ) ;
7353
7454 afterEach ( async ( ) => {
75- // Restore original config env vars
76- if ( originalHome ) {
77- process . env . HOME = originalHome ;
78- } else {
79- delete process . env . HOME ;
80- }
81-
82- if ( originalXdgConfig ) {
83- process . env . XDG_CONFIG_HOME = originalXdgConfig ;
84- } else {
85- delete process . env . XDG_CONFIG_HOME ;
86- }
55+ await restoreTestEnv ( ) ;
8756 } ) ;
8857
8958 it ( "should throw error when diagram parameter is missing" , async ( ) => {
9059 await expect (
91- handleMermaidPreview ( {
92- diagram : undefined ,
93- preview_id : testPreviewId ,
94- } )
60+ handleMermaidPreview ( { diagram : undefined , preview_id : testPreviewId } )
9561 ) . rejects . toThrow ( "diagram parameter is required" ) ;
9662 } ) ;
9763
9864 it ( "should throw error when preview_id parameter is missing" , async ( ) => {
9965 await expect (
100- handleMermaidPreview ( {
101- diagram : "graph TD; A-->B" ,
102- preview_id : undefined ,
103- } )
66+ handleMermaidPreview ( { diagram : "graph TD; A-->B" , preview_id : undefined } )
10467 ) . rejects . toThrow ( "preview_id parameter is required" ) ;
10568 } ) ;
10669
@@ -176,69 +139,44 @@ describe("handleMermaidPreview", () => {
176139
177140 it ( "should include stderr details in error when rendering fails" , async ( ) => {
178141 const mockExecFile = vi . mocked ( execFile ) ;
179- const originalImpl = mockExecFile . getMockImplementation ( ) ! ;
180-
181- try {
182- mockExecFile . mockImplementation ( ( _file : string , _args : any , callback : any ) => {
183- const error : any = new Error ( "Command failed: npx mmdc" ) ;
184- error . stderr = "Parse error on line 3: invalid syntax near 'graph'" ;
185- callback ( error , { stdout : "" , stderr : error . stderr } ) ;
186- } ) ;
142+ mockExecFile . mockImplementationOnce ( ( _file : string , _args : any , callback : any ) => {
143+ const error : any = new Error ( "Command failed: npx mmdc" ) ;
144+ error . stderr = "Parse error on line 3: invalid syntax near 'graph'" ;
145+ callback ( error , { stdout : "" , stderr : error . stderr } ) ;
146+ } ) ;
187147
188- const result = await handleMermaidPreview ( {
189- diagram : "invalid diagram syntax" ,
190- preview_id : testPreviewId ,
191- } ) ;
148+ const result = await handleMermaidPreview ( {
149+ diagram : "invalid diagram syntax" ,
150+ preview_id : testPreviewId ,
151+ } ) ;
192152
193- expect ( result . isError ) . toBe ( true ) ;
194- expect ( result . content [ 0 ] . text ) . toContain ( "Parse error on line 3" ) ;
195- expect ( result . content [ 0 ] . text ) . toContain ( "Command failed" ) ;
196- } finally {
197- mockExecFile . mockImplementation ( originalImpl ) ;
198- }
153+ expect ( result . isError ) . toBe ( true ) ;
154+ expect ( result . content [ 0 ] . text ) . toContain ( "Parse error on line 3" ) ;
155+ expect ( result . content [ 0 ] . text ) . toContain ( "Command failed" ) ;
199156 } ) ;
200157
201158 it ( "should show original error message when stderr is empty" , async ( ) => {
202159 const mockExecFile = vi . mocked ( execFile ) ;
203- const originalImpl = mockExecFile . getMockImplementation ( ) ! ;
204-
205- try {
206- mockExecFile . mockImplementation ( ( _file : string , _args : any , callback : any ) => {
207- const error = new Error ( "Command failed: npx mmdc" ) ;
208- callback ( error , { stdout : "" , stderr : "" } ) ;
209- } ) ;
160+ mockExecFile . mockImplementationOnce ( ( _file : string , _args : any , callback : any ) => {
161+ const error = new Error ( "Command failed: npx mmdc" ) ;
162+ callback ( error , { stdout : "" , stderr : "" } ) ;
163+ } ) ;
210164
211- const result = await handleMermaidPreview ( {
212- diagram : "invalid diagram syntax" ,
213- preview_id : testPreviewId ,
214- } ) ;
165+ const result = await handleMermaidPreview ( {
166+ diagram : "invalid diagram syntax" ,
167+ preview_id : testPreviewId ,
168+ } ) ;
215169
216- expect ( result . isError ) . toBe ( true ) ;
217- expect ( result . content [ 0 ] . text ) . toContain ( "Command failed" ) ;
218- } finally {
219- mockExecFile . mockImplementation ( originalImpl ) ;
220- }
170+ expect ( result . isError ) . toBe ( true ) ;
171+ expect ( result . content [ 0 ] . text ) . toContain ( "Command failed" ) ;
221172 } ) ;
222173} ) ;
223174
224175describe ( "handleMermaidSave" , ( ) => {
225176 const testPreviewId = "test-save" ;
226- let testDir : string ;
227- let originalHome : string | undefined ;
228- let originalXdgConfig : string | undefined ;
229177
230178 beforeEach ( async ( ) => {
231- // Override config dirs to use a temp HOME/XDG path for isolation
232- originalHome = process . env . HOME ;
233- originalXdgConfig = process . env . XDG_CONFIG_HOME ;
234- const tempHome = await mkdtemp ( join ( tmpdir ( ) , "claude-mermaid-test-home-" ) ) ;
235- const tempConfigDir = join ( tempHome , ".config" ) ;
236- process . env . HOME = tempHome ;
237- process . env . XDG_CONFIG_HOME = tempConfigDir ;
238-
239- await mkdir ( tempConfigDir , { recursive : true } ) ;
240- testDir = getPreviewDir ( testPreviewId ) ;
241- await mkdir ( testDir , { recursive : true } ) ;
179+ await setupTestEnvWithPreview ( testPreviewId ) ;
242180
243181 await handleMermaidPreview ( {
244182 diagram : "graph TD; A-->B" ,
@@ -248,35 +186,18 @@ describe("handleMermaidSave", () => {
248186 } ) ;
249187
250188 afterEach ( async ( ) => {
251- // Restore original config env vars
252- if ( originalHome ) {
253- process . env . HOME = originalHome ;
254- } else {
255- delete process . env . HOME ;
256- }
257-
258- if ( originalXdgConfig ) {
259- process . env . XDG_CONFIG_HOME = originalXdgConfig ;
260- } else {
261- delete process . env . XDG_CONFIG_HOME ;
262- }
189+ await restoreTestEnv ( ) ;
263190 } ) ;
264191
265192 it ( "should throw error when save_path parameter is missing" , async ( ) => {
266193 await expect (
267- handleMermaidSave ( {
268- save_path : undefined ,
269- preview_id : testPreviewId ,
270- } )
194+ handleMermaidSave ( { save_path : undefined , preview_id : testPreviewId } )
271195 ) . rejects . toThrow ( "save_path parameter is required" ) ;
272196 } ) ;
273197
274198 it ( "should throw error when preview_id parameter is missing" , async ( ) => {
275199 await expect (
276- handleMermaidSave ( {
277- save_path : "./test.svg" ,
278- preview_id : undefined ,
279- } )
200+ handleMermaidSave ( { save_path : "./test.svg" , preview_id : undefined } )
280201 ) . rejects . toThrow ( "preview_id parameter is required" ) ;
281202 } ) ;
282203
@@ -320,10 +241,9 @@ describe("handleMermaidSave", () => {
320241 } ) ;
321242
322243 it ( "should handle missing diagram source when saving" , async ( ) => {
323- const nonExistentId = "non-existent-preview" ;
324244 const result = await handleMermaidSave ( {
325245 save_path : "/tmp/test-diagram.svg" ,
326- preview_id : nonExistentId ,
246+ preview_id : "non-existent-preview" ,
327247 } ) ;
328248
329249 expect ( result . isError ) . toBe ( true ) ;
0 commit comments