11// tslint:disable-file:no-shadowed-variable
22import { sync as spawnSync } from 'cross-spawn' ;
3- import { join } from 'path' ;
3+ import { join , relative , sep } from 'path' ;
44import * as Paths from '../../scripts/paths' ;
55import * as fs from 'fs-extra' ;
66
@@ -133,6 +133,8 @@ export interface RunTestOptions {
133133 template ?: string ;
134134 env ?: { } ;
135135 args ?: string [ ] ;
136+ inject ?: ( ( ) => any ) | string ;
137+ writeIo ?: boolean ;
136138}
137139
138140export interface TestRunResult {
@@ -141,6 +143,12 @@ export interface TestRunResult {
141143 stdout : string ;
142144 stderr : string ;
143145 output : string ;
146+ ioDataFor ( relPath : string ) : TestFileIoData ;
147+ }
148+ interface TestFileIoData {
149+ in : [ string , jest . Path , jest . ProjectConfig , jest . TransformOptions ?] ;
150+ // out: string | jest.TransformedSource;
151+ out : string ;
144152}
145153
146154// tslint:disable-next-line:interface-over-type-literal
@@ -194,27 +202,73 @@ export function templateNameForPath(path: string): string {
194202 return 'default' ;
195203}
196204
197- export function run (
198- name : string ,
199- { args = [ ] , env = { } , template } : RunTestOptions = { } ,
200- ) : TestRunResult {
201- const dir = prepareTest (
205+ export function run ( name : string , options : RunTestOptions = { } ) : TestRunResult {
206+ const { args = [ ] , env = { } , template, inject, writeIo } = options ;
207+ const { workdir : dir , sourceDir, hooksFile, ioDir } = prepareTest (
202208 name ,
203209 template || templateNameForPath ( join ( Paths . e2eSourceDir , name ) ) ,
210+ options ,
204211 ) ;
205212 const pkg = require ( join ( dir , 'package.json' ) ) ;
206213
207- const prefix =
208- pkg . scripts && pkg . scripts . test
209- ? [ 'npm' , '-s' , 'run' , 'test' ]
210- : [ join ( dir , 'node_modules' , '.bin' , 'jest' ) ] ;
211- args = [ ...prefix , ...args ] ;
212- const cmd = args . shift ( ) ;
214+ let cmdArgs : string [ ] = [ ] ;
215+ if ( inject ) {
216+ cmdArgs . push ( '--testPathPattern="/__eval\\\\.ts$"' ) ;
217+ } // '--testRegex=""'
218+ if ( process . argv . find ( v => [ '--updateSnapshot' , '-u' ] . includes ( v ) ) ) {
219+ cmdArgs . push ( '-u' ) ;
220+ }
221+ cmdArgs . push ( ...args ) ;
222+ if ( ! inject && pkg . scripts && pkg . scripts . test ) {
223+ if ( cmdArgs . length ) {
224+ cmdArgs . unshift ( '--' ) ;
225+ }
226+ cmdArgs = [ 'npm' , '-s' , 'run' , 'test' , ...cmdArgs ] ;
227+ } else {
228+ cmdArgs . unshift ( join ( dir , 'node_modules' , '.bin' , 'jest' ) ) ;
229+ }
230+
231+ const cmd = cmdArgs . shift ( ) ;
232+
233+ // Add both process.env which is the standard and custom env variables
234+ const mergedEnv : any = {
235+ ...process . env ,
236+ ...env ,
237+ } ;
238+ if ( inject ) {
239+ const injected =
240+ typeof inject === 'function'
241+ ? `(${ inject . toString ( ) } ).apply(this);`
242+ : inject ;
243+ mergedEnv . __TS_JEST_EVAL = injected ;
244+ }
245+ if ( writeIo ) {
246+ mergedEnv . __TS_JEST_HOOKS = hooksFile ;
247+ }
213248
214- const result = spawnSync ( cmd , args , {
249+ const result = spawnSync ( cmd , cmdArgs , {
215250 cwd : dir ,
216- // Add both process.env which is the standard and custom env variables
217- env : { ...process . env , ...env } ,
251+ env : mergedEnv ,
252+ } ) ;
253+
254+ // we need to copy each snapshot which does NOT exists in the source dir
255+ fs . readdirSync ( dir ) . forEach ( item => {
256+ if (
257+ item === 'node_modules' ||
258+ ! fs . statSync ( join ( dir , item ) ) . isDirectory ( )
259+ ) {
260+ return ;
261+ }
262+ const srcDir = join ( sourceDir , item ) ;
263+ const wrkDir = join ( dir , item ) ;
264+ fs . copySync ( wrkDir , srcDir , {
265+ overwrite : false ,
266+ filter : from => {
267+ return relative ( sourceDir , from )
268+ . split ( sep )
269+ . includes ( '__snapshots__' ) ;
270+ } ,
271+ } ) ;
218272 } ) ;
219273
220274 // Call to string on byte arrays and strip ansi color codes for more accurate string comparison.
@@ -224,13 +278,19 @@ export function run(
224278 ? stripAnsiColors ( result . output . join ( '\n\n' ) )
225279 : '' ;
226280
227- return {
281+ const res = {
228282 [ TestRunResultFlag ] : true ,
229283 status : result . status ,
230284 stderr,
231285 stdout,
232286 output,
233287 } ;
288+ if ( writeIo ) {
289+ Object . defineProperty ( res , 'ioDataFor' , {
290+ value : ( relPath : string ) => require ( `${ ioDir } /${ relPath } .json` ) ,
291+ } ) ;
292+ }
293+ return res as any ;
234294}
235295
236296// from https://stackoverflow.com/questions/25245716/remove-all-ansi-colors-styles-from-strings
@@ -241,18 +301,29 @@ function stripAnsiColors(stringToStrip: string): string {
241301 ) ;
242302}
243303
244- function prepareTest ( name : string , template : string ) : string {
304+ interface PreparedTest {
305+ workdir : string ;
306+ templateDir : string ;
307+ sourceDir : string ;
308+ ioDir : string ;
309+ hooksFile : string ;
310+ }
311+ function prepareTest (
312+ name : string ,
313+ template : string ,
314+ options : RunTestOptions = { } ,
315+ ) : PreparedTest {
245316 const sourceDir = join ( Paths . e2eSourceDir , name ) ;
246317 // working directory is in the temp directory, different for each template name
247- const caseDir = join ( Paths . e2eWorkDir , template , name ) ;
318+ const caseWorkdir = join ( Paths . e2eWorkDir , template , name ) ;
248319 const templateDir = join ( Paths . e2eWorkTemplatesDir , template ) ;
249320
250321 // recreate the directory
251- fs . removeSync ( caseDir ) ;
252- fs . mkdirpSync ( caseDir ) ;
322+ fs . removeSync ( caseWorkdir ) ;
323+ fs . mkdirpSync ( caseWorkdir ) ;
253324
254325 const tmplModulesDir = join ( templateDir , 'node_modules' ) ;
255- const caseModulesDir = join ( caseDir , 'node_modules' ) ;
326+ const caseModulesDir = join ( caseWorkdir , 'node_modules' ) ;
256327
257328 // link the node_modules dir if the template has one
258329 if ( fs . existsSync ( tmplModulesDir ) ) {
@@ -264,19 +335,82 @@ function prepareTest(name: string, template: string): string {
264335 if ( TEMPLATE_EXCLUDED_ITEMS . includes ( item ) ) {
265336 return ;
266337 }
267- fs . copySync ( join ( templateDir , item ) , join ( caseDir , item ) ) ;
338+ fs . copySync ( join ( templateDir , item ) , join ( caseWorkdir , item ) ) ;
268339 } ) ;
269340
270341 // copy source and test files
271- fs . copySync ( sourceDir , caseDir ) ;
342+ const snapshotDirs : Record < string , 0 > = Object . create ( null ) ;
343+ fs . copySync ( sourceDir , caseWorkdir , {
344+ filter : src => {
345+ const relPath = relative ( sourceDir , src ) ;
346+ const segments = relPath . split ( sep ) ;
347+ if ( segments . includes ( '__snapshots__' ) ) {
348+ // link snapshots
349+ while ( segments [ segments . length - 1 ] !== '__snapshots__' ) {
350+ segments . pop ( ) ;
351+ }
352+ snapshotDirs [ segments . join ( sep ) ] = 0 ;
353+ return false ;
354+ } else {
355+ return true ;
356+ }
357+ } ,
358+ } ) ;
359+ // create symbolic links for the existing snapshots
360+ Object . keys ( snapshotDirs ) . forEach ( dir => {
361+ fs . ensureSymlinkSync ( join ( sourceDir , dir ) , join ( caseWorkdir , dir ) ) ;
362+ } ) ;
363+
364+ // create the special files
365+ fs . outputFileSync ( join ( caseWorkdir , '__eval.ts' ) , EVAL_SOURCE , 'utf8' ) ;
366+ let ioDir ! : string ;
367+ if ( options . writeIo ) {
368+ ioDir = join ( caseWorkdir , '__io' ) ;
369+ fs . mkdirpSync ( ioDir ) ;
370+ }
371+ const hooksFile = join ( caseWorkdir , '__hooks.js' ) ;
372+ fs . outputFileSync (
373+ hooksFile ,
374+ hooksSourceWith ( {
375+ writeProcessIoTo : ioDir || false ,
376+ } ) ,
377+ 'utf8' ,
378+ ) ;
272379
273380 // create a package.json if it does not exists, and/or enforce the package name
274- const pkgFile = join ( caseDir , 'package.json' ) ;
381+ const pkgFile = join ( caseWorkdir , 'package.json' ) ;
275382 const pkg : any = fs . existsSync ( pkgFile ) ? fs . readJsonSync ( pkgFile ) : { } ;
276383 pkg . name = name ;
277384 pkg . private = true ;
278385 pkg . version = `0.0.0-mock0` ;
279386 fs . outputJsonSync ( pkgFile , pkg , { spaces : 2 } ) ;
280387
281- return caseDir ;
388+ return { workdir : caseWorkdir , templateDir, sourceDir, hooksFile, ioDir } ;
389+ }
390+
391+ const EVAL_SOURCE = `
392+ describe.skip('__eval', () => {
393+ test.skip('__test', () => {
394+ expect(true).toBe(true);
395+ });
396+ it.skip('__test', () => {
397+ expect(true).toBe(true);
398+ });
399+ });
400+
401+ eval(process.__TS_JEST_EVAL);
402+ ` ;
403+
404+ // tslint:disable-next-line:variable-name
405+ let __hooksSource : string ;
406+ function hooksSourceWith ( vars : Record < string , any > ) : string {
407+ if ( ! __hooksSource ) {
408+ __hooksSource = fs . readFileSync (
409+ join ( __dirname , '__hooks-source__.js.hbs' ) ,
410+ 'utf8' ,
411+ ) ;
412+ }
413+ return __hooksSource . replace ( / \{ \{ ( [ ^ \} ] + ) \} \} / g, ( _ , key ) =>
414+ JSON . stringify ( vars [ key ] ) ,
415+ ) ;
282416}
0 commit comments