11import { afterEach , beforeEach , describe , expect , it , MockInstance , vi } from 'vitest' ;
2- import type { IReporterOutput } from 'dependency-cruiser' ;
2+ import type { ICruiseResult , IModule , IReporterOutput } from 'dependency-cruiser' ;
33
4- // 1. Mock dependency-cruiser to control the output of `cruise`
4+ // 1. Mock dependency-cruiser to control the output of `cruise` and `format`
55vi . mock ( 'dependency-cruiser' , ( ) => ( {
66 cruise : vi . fn ( ) ,
7+ format : vi . fn ( ) ,
78} ) ) ;
89
910// 2. Mock the log/panic module
@@ -14,70 +15,149 @@ vi.mock('../lib/log.js', () => ({
1415} ) ) ;
1516
1617// 3. Import the mocked modules and the function under test
17- const { cruise } = await import ( 'dependency-cruiser' ) ;
18+ const { cruise, format } = await import ( 'dependency-cruiser' ) ;
1819const { panic } = await import ( '../lib/log.js' ) ;
1920const { generateDependencyGraph } = await import ( './deps-graph.js' ) ;
2021
22+ /** Build a minimal ICruiseResult with the given modules; all other fields stubbed. */
23+ function fakeCruiseResult ( modules : Pick < IModule , 'source' | 'dependencies' > [ ] ) : ICruiseResult {
24+ return {
25+ modules : modules . map ( ( m ) => ( {
26+ source : m . source ,
27+ dependencies : m . dependencies ,
28+ dependents : [ ] ,
29+ valid : true ,
30+ } ) ) as IModule [ ] ,
31+ summary : { } as ICruiseResult [ 'summary' ] ,
32+ } ;
33+ }
34+
2135describe ( 'generateDependencyGraph' , ( ) => {
2236 let mockStdoutWrite : MockInstance < typeof process . stdout . write > ;
2337
2438 beforeEach ( ( ) => {
25- // Reset and clear mocks before each test
2639 vi . clearAllMocks ( ) ;
27-
28- // Spy on process.stdout.write to check what gets written
2940 mockStdoutWrite = vi . spyOn ( process . stdout , 'write' ) . mockImplementation ( ( ) => true ) ;
41+
42+ // Default: cruise returns the modules-shaped result the new code expects
43+ vi . mocked ( cruise ) . mockResolvedValue ( {
44+ output : fakeCruiseResult ( [ { source : 'src/a.ts' , dependencies : [ ] } ] ) ,
45+ } as IReporterOutput ) ;
46+
47+ // Default: format returns a minimal mermaid string the post-processor can rewrite
48+ vi . mocked ( format ) . mockResolvedValue ( { output : 'flowchart LR\nA-->B' } as IReporterOutput ) ;
3049 } ) ;
3150
3251 afterEach ( ( ) => {
33- // Restore any spied methods
3452 mockStdoutWrite . mockRestore ( ) ;
3553 } ) ;
3654
3755 it ( 'generates a mermaid diagram, replacing flowchart LR with flowchart TB' , async ( ) => {
38- // Mock a successful result from dependency-cruiser
39- vi . mocked ( cruise ) . mockResolvedValueOnce ( {
40- output : 'flowchart LR\nA-->B' ,
41- } as IReporterOutput ) ;
42-
4356 await expect ( generateDependencyGraph ( 'src' ) ) . resolves . toBeUndefined ( ) ;
4457
45- // Verify the call to `cruise` used the expected arguments
46- expect ( cruise ) . toHaveBeenCalledWith ( [ 'src' ] , expect . any ( Object ) ) ;
58+ expect ( cruise ) . toHaveBeenCalledWith ( [ 'src' ] , expect . objectContaining ( { outputType : 'json' } ) ) ;
4759
48- // After modification, we expect flowchart TB in the output
4960 expect ( mockStdoutWrite ) . toHaveBeenCalledTimes ( 1 ) ;
50- // The actual argument passed to stdout.write
51- const writtenBuffer = mockStdoutWrite . mock . calls [ 0 ] [ 0 ] as Buffer ;
52- const writtenString = writtenBuffer . toString ( ) ;
53-
61+ const writtenString = ( mockStdoutWrite . mock . calls [ 0 ] [ 0 ] as Buffer ) . toString ( ) ;
5462 expect ( writtenString ) . toMatch ( / ^ ` ` ` m e r m a i d \n / ) ;
5563 expect ( writtenString ) . toMatch ( / \n f l o w c h a r t T B \n A - - > B \n / ) ;
5664 expect ( writtenString ) . toMatch ( / \n ` ` ` \n $ / ) ;
5765 } ) ;
5866
5967 it ( 'panics if dependency-cruiser throws an error' , async ( ) => {
60- // Simulate a thrown error from `cruise`
6168 vi . mocked ( cruise ) . mockImplementationOnce ( async ( ) => {
6269 throw new Error ( 'Some error from dependency-cruiser' ) ;
6370 } ) ;
6471
6572 await expect ( generateDependencyGraph ( 'bad/path' ) ) . rejects . toThrow ( 'Some error from dependency-cruiser' ) ;
6673
67- // Confirm the panic function was called with the thrown error message
6874 expect ( panic ) . toHaveBeenCalledWith ( 'Error: Some error from dependency-cruiser' ) ;
69- // No output should be written
7075 expect ( mockStdoutWrite ) . not . toHaveBeenCalled ( ) ;
7176 } ) ;
7277
73- it ( 'panics if the returned output is not a string' , async ( ) => {
74- // Mock an invalid result (missing or incorrect `output` field)
75- vi . mocked ( cruise ) . mockResolvedValueOnce ( {
76- output : null ,
77- } as unknown as IReporterOutput ) ;
78+ it ( 'panics if the formatted output is not a string' , async ( ) => {
79+ vi . mocked ( format ) . mockResolvedValueOnce ( { output : null } as unknown as IReporterOutput ) ;
7880
7981 await expect ( generateDependencyGraph ( 'src' ) ) . rejects . toThrow ( 'no output' ) ;
8082 expect ( panic ) . toHaveBeenCalledWith ( 'no output' ) ;
8183 expect ( mockStdoutWrite ) . not . toHaveBeenCalled ( ) ;
8284 } ) ;
85+
86+ describe ( '--exclude' , ( ) => {
87+ it ( 'passes user-provided globs (as regex) to cruise alongside built-in excludes' , async ( ) => {
88+ await generateDependencyGraph ( 'src' , { exclude : [ '**/_planned.ts' ] } ) ;
89+
90+ const opts = vi . mocked ( cruise ) . mock . calls [ 0 ] [ 1 ] ;
91+ expect ( opts ) . toBeDefined ( ) ;
92+ const excludePatterns = ( opts ! . exclude ?? [ ] ) as string [ ] ;
93+ // built-in excludes are still present
94+ expect ( excludePatterns ) . toContain ( '\\.(test|d|mock)\\.ts$' ) ;
95+ // user glob translated to a regex
96+ expect ( excludePatterns . some ( ( p ) => / _ p l a n n e d / . test ( p ) ) ) . toBe ( true ) ;
97+ } ) ;
98+ } ) ;
99+
100+ describe ( '--collapse-dir' , ( ) => {
101+ it ( 'merges files matching a glob into a single node and reports the count' , async ( ) => {
102+ vi . mocked ( cruise ) . mockResolvedValueOnce ( {
103+ output : fakeCruiseResult ( [
104+ {
105+ source : 'src/regions/index.ts' ,
106+ dependencies : [
107+ { resolved : 'src/regions/de.ts' } ,
108+ { resolved : 'src/regions/fr.ts' } ,
109+ { resolved : 'src/regions/it.ts' } ,
110+ { resolved : 'src/regions/lib.ts' } ,
111+ ] as IModule [ 'dependencies' ] ,
112+ } ,
113+ {
114+ source : 'src/regions/de.ts' ,
115+ dependencies : [ { resolved : 'src/regions/lib.ts' } ] as IModule [ 'dependencies' ] ,
116+ } ,
117+ {
118+ source : 'src/regions/fr.ts' ,
119+ dependencies : [ { resolved : 'src/regions/lib.ts' } ] as IModule [ 'dependencies' ] ,
120+ } ,
121+ {
122+ source : 'src/regions/it.ts' ,
123+ dependencies : [ { resolved : 'src/regions/lib.ts' } ] as IModule [ 'dependencies' ] ,
124+ } ,
125+ { source : 'src/regions/lib.ts' , dependencies : [ ] } ,
126+ ] ) ,
127+ } as IReporterOutput ) ;
128+
129+ await generateDependencyGraph ( 'src' , { collapseDir : [ 'src/regions/{de,fr,it}.ts' ] } ) ;
130+
131+ // The collapsed result is what `format` receives.
132+ const formatArg = vi . mocked ( format ) . mock . calls [ 0 ] [ 0 ] as ICruiseResult ;
133+ const sources = formatArg . modules . map ( ( m ) => m . source ) . sort ( ) ;
134+
135+ // Original 5 files reduced to 3: index, lib, and one merged regions/{de,fr,it}.ts node
136+ expect ( formatArg . modules ) . toHaveLength ( 3 ) ;
137+ expect ( sources ) . toEqual ( [ 'src/regions/index.ts' , 'src/regions/lib.ts' , 'src/regions/{de,fr,it}.ts (3 files)' ] ) ;
138+
139+ // Edges from index.ts: the three collapsed files become a single deduped edge to the merged node.
140+ const indexNode = formatArg . modules . find ( ( m ) => m . source === 'src/regions/index.ts' ) ! ;
141+ const indexResolved = indexNode . dependencies . map ( ( d ) => d . resolved ) . sort ( ) ;
142+ expect ( indexResolved ) . toEqual ( [ 'src/regions/lib.ts' , 'src/regions/{de,fr,it}.ts (3 files)' ] ) ;
143+
144+ // The merged node depends on lib.ts (deduped from the three originals) — and has no self-loop.
145+ const merged = formatArg . modules . find ( ( m ) => m . source . startsWith ( 'src/regions/{de,fr,it}.ts' ) ) ! ;
146+ expect ( merged . dependencies . map ( ( d ) => d . resolved ) ) . toEqual ( [ 'src/regions/lib.ts' ] ) ;
147+ } ) ;
148+
149+ it ( 'does nothing when no files match the collapse glob' , async ( ) => {
150+ vi . mocked ( cruise ) . mockResolvedValueOnce ( {
151+ output : fakeCruiseResult ( [
152+ { source : 'src/a.ts' , dependencies : [ { resolved : 'src/b.ts' } ] as IModule [ 'dependencies' ] } ,
153+ { source : 'src/b.ts' , dependencies : [ ] } ,
154+ ] ) ,
155+ } as IReporterOutput ) ;
156+
157+ await generateDependencyGraph ( 'src' , { collapseDir : [ 'src/no-match/*.ts' ] } ) ;
158+
159+ const formatArg = vi . mocked ( format ) . mock . calls [ 0 ] [ 0 ] as ICruiseResult ;
160+ expect ( formatArg . modules . map ( ( m ) => m . source ) . sort ( ) ) . toEqual ( [ 'src/a.ts' , 'src/b.ts' ] ) ;
161+ } ) ;
162+ } ) ;
83163} ) ;
0 commit comments