@@ -9,62 +9,111 @@ import path from 'path';
99import expect from '@kbn/expect' ;
1010// @ts -expect-error
1111import prettier from 'prettier' ;
12- import { once } from 'lodash' ;
1312// @ts -expect-error
1413import babelTraverse from '@babel/traverse' ;
14+ import { Suite , Test } from 'mocha' ;
1515
1616type ISnapshotState = InstanceType < typeof SnapshotState > ;
1717
18- let testContext : {
19- file : string ;
20- testTitle : string ;
21- getSnapshotContext : ( ) => SnapshotContext ;
22- } | null = null ;
2318interface SnapshotContext {
2419 snapshotState : ISnapshotState ;
20+ currentTestName : string ;
2521}
2622
23+ let testContext : {
24+ file : string ;
25+ snapshotTitle : string ;
26+ snapshotContext : SnapshotContext ;
27+ } | null = null ;
28+
2729let registered : boolean = false ;
2830
31+ function getSnapshotMeta ( currentTest : Test ) {
32+ // Make sure snapshot title is unique per-file, rather than entire
33+ // suite. This allows reuse of tests, for instance to compare
34+ // results for different configurations.
35+
36+ const titles = [ currentTest . title ] ;
37+ const file = currentTest . file ;
38+
39+ let test : Suite | undefined = currentTest ?. parent ;
40+
41+ while ( test && test . file === file ) {
42+ titles . push ( test . title ) ;
43+ test = test . parent ;
44+ }
45+
46+ const snapshotTitle = titles . reverse ( ) . join ( ' ' ) ;
47+
48+ if ( ! file || ! snapshotTitle ) {
49+ throw new Error ( `file or snapshotTitle not available in Mocha test context` ) ;
50+ }
51+
52+ return {
53+ file,
54+ snapshotTitle,
55+ } ;
56+ }
57+
2958export function registerMochaHooksForSnapshots ( ) {
30- const snapshotsToSave : ISnapshotState [ ] = [ ] ;
59+ let snapshotStatesByFilePath : Record < string , ISnapshotState > = { } ;
3160
3261 registered = true ;
3362
3463 beforeEach ( function ( ) {
35- const mochaContext = this ;
36- const file = mochaContext . currentTest ?. file ;
37- const testTitle = mochaContext . currentTest ?. fullTitle ( ) ;
64+ const { file, snapshotTitle } = getSnapshotMeta ( this . currentTest ! ) ;
3865
39- if ( ! file || ! testTitle ) {
40- throw new Error ( ` file or fullTitle not found in Mocha test context` ) ;
66+ if ( ! snapshotStatesByFilePath [ file ] ) {
67+ snapshotStatesByFilePath [ file ] = getSnapshotState ( file ) ;
4168 }
4269
4370 testContext = {
4471 file,
45- testTitle,
46- getSnapshotContext : once ( ( ) => {
47- const ctx = getSnapshotContextOrThrow ( { file, testTitle } ) ;
48- snapshotsToSave . push ( ctx . snapshotState ) ;
49- return ctx ;
50- } ) ,
72+ snapshotTitle,
73+ snapshotContext : {
74+ snapshotState : snapshotStatesByFilePath [ file ] ,
75+ currentTestName : snapshotTitle ,
76+ } ,
5177 } ;
5278 } ) ;
5379
54- afterEach ( ( ) => {
80+ afterEach ( function ( ) {
81+ if ( ! this . currentTest ?. isPassed ( ) ) {
82+ const { file, snapshotTitle } = getSnapshotMeta ( this . currentTest ! ) ;
83+ snapshotStatesByFilePath [ file ] . markSnapshotsAsCheckedForTest ( snapshotTitle ) ;
84+ }
85+
5586 testContext = null ;
5687 } ) ;
5788
58- after ( ( ) => {
59- // save snapshot after tests complete, in reverse order (bottom to top)
60- // to not change line/column number of successive inline snapshot tests
61- snapshotsToSave
62- . concat ( )
63- . reverse ( )
64- . forEach ( ( snapshot ) => {
65- snapshot . save ( ) ;
66- } ) ;
67- snapshotsToSave . length = 0 ;
89+ after ( function ( ) {
90+ // save snapshot after tests complete
91+
92+ const unused : string [ ] = [ ] ;
93+
94+ const isUpdatingSnapshots = process . env . UPDATE_SNAPSHOTS ;
95+
96+ Object . keys ( snapshotStatesByFilePath ) . forEach ( ( file ) => {
97+ const snapshot = snapshotStatesByFilePath [ file ] ;
98+
99+ if ( ! isUpdatingSnapshots ) {
100+ unused . push ( ...snapshot . getUncheckedKeys ( ) ) ;
101+ } else {
102+ snapshot . removeUncheckedKeys ( ) ;
103+ }
104+
105+ snapshot . save ( ) ;
106+ } ) ;
107+
108+ if ( unused . length ) {
109+ throw new Error (
110+ `${ unused . length } obsolete snapshot(s) found:\n${ unused . join (
111+ '\n\t'
112+ ) } .\n\nRun tests again with \`UPDATE_SNAPSHOTS=1\` to remove them.`
113+ ) ;
114+ }
115+
116+ snapshotStatesByFilePath = { } ;
68117
69118 registered = false ;
70119 } ) ;
@@ -85,23 +134,20 @@ Error.prepareStackTrace = (error, structuredStackTrace) => {
85134 }
86135} ;
87136
88- function getSnapshotContextOrThrow ( { file, testTitle } : { file : string ; testTitle : string } ) {
137+ function getSnapshotState ( file : string ) {
89138 const dirname = path . dirname ( file ) ;
90139 const filename = path . basename ( file ) ;
91140
92141 const snapshotState = new SnapshotState (
93142 path . join ( dirname + `/__snapshots__/` + filename . replace ( path . extname ( filename ) , '.snap' ) ) ,
94143 {
95- updateSnapshot : process . env . UPDATE_APM_SNAPSHOTS ? 'all' : 'new' ,
144+ updateSnapshot : process . env . UPDATE_SNAPSHOTS ? 'all' : 'new' ,
96145 getPrettier : ( ) => prettier ,
97146 getBabelTraverse : ( ) => babelTraverse ,
98147 }
99148 ) ;
100149
101- return {
102- snapshotState,
103- currentTestName : testTitle ,
104- } as SnapshotContext ;
150+ return snapshotState ;
105151}
106152
107153export function expectSnapshot ( received : any ) {
@@ -115,23 +161,26 @@ export function expectSnapshot(received: any) {
115161 throw new Error ( 'A current Mocha context is needed to match snapshots' ) ;
116162 }
117163
118- const snapshotContext = testContext . getSnapshotContext ( ) ;
119-
120164 return {
121- toMatch : expectToMatchSnapshot . bind ( snapshotContext , received ) ,
122- toMatchInline : expectToMatchInlineSnapshot . bind ( snapshotContext , received ) ,
165+ toMatch : expectToMatchSnapshot . bind ( null , testContext . snapshotContext , received ) ,
166+ // use bind to support optional 3rd argument (actual)
167+ toMatchInline : expectToMatchInlineSnapshot . bind ( null , testContext . snapshotContext , received ) ,
123168 } ;
124169}
125170
126- function expectToMatchSnapshot ( this : SnapshotContext , received : any ) {
127- const matcher = toMatchSnapshot . bind ( this as any ) ;
171+ function expectToMatchSnapshot ( snapshotContext : SnapshotContext , received : any ) {
172+ const matcher = toMatchSnapshot . bind ( snapshotContext as any ) ;
128173 const result = matcher ( received ) ;
129174
130175 expect ( result . pass ) . to . eql ( true , result . message ( ) ) ;
131176}
132177
133- function expectToMatchInlineSnapshot ( this : SnapshotContext , received : any , _actual ?: any ) {
134- const matcher = toMatchInlineSnapshot . bind ( this as any ) ;
178+ function expectToMatchInlineSnapshot (
179+ snapshotContext : SnapshotContext ,
180+ received : any ,
181+ _actual ?: any
182+ ) {
183+ const matcher = toMatchInlineSnapshot . bind ( snapshotContext as any ) ;
135184
136185 const result = arguments . length === 1 ? matcher ( received ) : matcher ( received , _actual ) ;
137186
0 commit comments