yeoman-test 6.2.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,552 +0,0 @@
1
- 'use strict';
2
- const crypto = require('crypto');
3
- const fs = require('fs');
4
- const path = require('path');
5
- const assert = require('assert');
6
- const _ = require('lodash/string');
7
- const util = require('util');
8
- const {EventEmitter} = require('events');
9
- const tempDirectory = require('temp-dir');
10
-
11
- const RunResult = require('./run-result');
12
-
13
- /**
14
- * This class provide a run context object to façade the complexity involved in setting
15
- * up a generator for testing
16
- * @constructor
17
- * @param {String|Function} Generator - Namespace or generator constructor. If the later
18
- * is provided, then namespace is assumed to be
19
- * 'gen:test' in all cases
20
- * @param {Object} [settings]
21
- * @param {Boolean} [settings.tmpdir] - Automatically run this generator in a tmp dir
22
- * @param {String} [settings.resolved] - File path to the generator (only used if Generator is a constructor)
23
- * @param {String} [settings.namespace='gen:test'] - Namespace (only used if Generator is a constructor)
24
- * @param {String} [settings.runEnvironment=false] - Require the run context to call run.
25
- * @param {Object} [envOptions] - Options to be passed to environment.
26
- * @return {this}
27
- */
28
-
29
- function RunContext(
30
- Generator,
31
- settings,
32
- envOptions = {},
33
- helpers = require('.')
34
- ) {
35
- this._asyncHolds = 0;
36
- this.ran = false;
37
- this.inDirSet = false;
38
- this.args = [];
39
- this.options = {};
40
- this.answers = {};
41
- this.localConfig = null;
42
- this.dependencies = [];
43
- this.Generator = Generator;
44
- this.inDirCallbacks = [];
45
- this.lookups = [];
46
- this.mockedGenerators = {};
47
- this.settings = {
48
- namespace: 'gen:test',
49
- runEnvironment: false,
50
- ...settings
51
- };
52
- this.envOptions = envOptions;
53
-
54
- this.withOptions({
55
- force: true,
56
- skipCache: true,
57
- skipInstall: true
58
- });
59
- this.oldCwd = this.settings.oldCwd;
60
- if (this.settings.cwd) {
61
- this.cd(this.settings.cwd);
62
- }
63
-
64
- if (!this.settings.runEnvironment) {
65
- setTimeout(this._run.bind(this), 10);
66
- }
67
-
68
- this.helpers = helpers;
69
- }
70
-
71
- util.inherits(RunContext, EventEmitter);
72
-
73
- /**
74
- * Hold the execution until the returned callback is triggered
75
- * @return {Function} Callback to notify the normal execution can resume
76
- */
77
-
78
- RunContext.prototype.async = function () {
79
- this._asyncHolds++;
80
-
81
- return function () {
82
- this._asyncHolds--;
83
- this._run();
84
- }.bind(this);
85
- };
86
-
87
- /**
88
- * Build the generator and the environment.
89
- * @return {RunContext|false} this
90
- */
91
- RunContext.prototype.build = function (callback = () => {}) {
92
- if (!this.inDirSet && this.settings.tmpdir !== false) {
93
- this.inTmpDir();
94
- }
95
-
96
- if (this._asyncHolds !== 0 || this.ran || this.completed) {
97
- if (this.buildAsync) {
98
- return false;
99
- }
100
-
101
- throw new Error('The context is not ready');
102
- }
103
-
104
- this.ran = true;
105
- if (this.inDirCallbacks.length > 0) {
106
- const targetDirectory = path.resolve(this.targetDirectory);
107
- this.inDirCallbacks.forEach((cb) => cb(targetDirectory));
108
- }
109
-
110
- this.targetDirectory = this.targetDirectory || process.cwd();
111
-
112
- const testEnv = this.helpers.createTestEnv(this.envOptions.createEnv, {
113
- cwd: this.settings.forwardCwd ? this.targetDirectory : undefined,
114
- ...this.options,
115
- ...this.envOptions
116
- });
117
- this.env = this.envCB ? this.envCB(testEnv) || testEnv : testEnv;
118
-
119
- this.lookups.forEach((lookup) => {
120
- this.env.lookup(lookup);
121
- });
122
-
123
- this.helpers.registerDependencies(this.env, this.dependencies);
124
-
125
- let namespace;
126
- if (typeof this.Generator === 'string') {
127
- if (this.settings.runEnvironment) {
128
- namespace = this.Generator;
129
- } else {
130
- namespace = this.env.namespace(this.Generator);
131
- if (namespace !== this.Generator) {
132
- // Generator is a file path, it should be registered.
133
- this.env.register(this.Generator);
134
- }
135
- }
136
- } else {
137
- namespace = this.settings.namespace;
138
- this.env.registerStub(this.Generator, namespace, this.settings.resolved);
139
- }
140
-
141
- this._generatorPromise = Promise.resolve(
142
- this.env.create(namespace, {
143
- arguments: this.args,
144
- options: this.options
145
- })
146
- );
147
-
148
- this._generatorPromise.then((generator) => {
149
- this.generator = generator;
150
- });
151
-
152
- this.helpers.mockPrompt(this.env, this.answers, this.promptOptions);
153
-
154
- if (this.localConfig) {
155
- // Only mock local config when withLocalConfig was called
156
- this._generatorPromise = this._generatorPromise.then((generator) => {
157
- this.helpers.mockLocalConfig(generator, this.localConfig);
158
- return generator;
159
- });
160
- }
161
-
162
- callback(this);
163
- return this;
164
- };
165
-
166
- /**
167
- * Method called when the context is ready to run the generator
168
- * @private
169
- */
170
-
171
- RunContext.prototype._run = function () {
172
- this.buildAsync = true;
173
- if (this.build() === false) return false;
174
-
175
- this._generatorPromise.then((generator) => this.emit('ready', generator));
176
-
177
- this.run()
178
- .catch((error) => {
179
- if (
180
- this.listenerCount('end') === 0 &&
181
- this.listenerCount('error') === 0
182
- ) {
183
- // When there is no listeners throw a unhandled rejection.
184
- setImmediate(function () {
185
- return Promise.reject(error);
186
- });
187
- } else {
188
- this.errored = true;
189
- this.emit('error', error);
190
- }
191
- })
192
- .finally(() => {
193
- this.emit('end');
194
- this.completed = true;
195
- });
196
-
197
- return true;
198
- };
199
-
200
- /**
201
- * Run the generator on the environment and promises a RunResult instance.
202
- * @return {Promise<RunResult>} Promise a RunResult instance.
203
- */
204
- RunContext.prototype.run = function () {
205
- if (!this.settings.runEnvironment && this.buildAsync === undefined) {
206
- throw new Error('Should be called with runEnvironment option');
207
- }
208
-
209
- if (!this.ran) {
210
- this.build();
211
- }
212
-
213
- return this._generatorPromise.then((generator) =>
214
- this.env
215
- .runGenerator(generator)
216
- .then(() => new RunResult(this._createRunResultOptions()))
217
- .finally(() => {
218
- this.helpers.restorePrompt(this.env);
219
- })
220
- );
221
- };
222
-
223
- RunContext.prototype._createRunResultOptions = function () {
224
- return {
225
- env: this.env,
226
- generator: this.generator,
227
- memFs: this.env.sharedFs,
228
- settings: {
229
- ...this.settings
230
- },
231
- oldCwd: this.oldCwd,
232
- cwd: this.targetDirectory,
233
- envOptions: this.envOptions,
234
- mockedGenerators: this.mockedGenerators
235
- };
236
- };
237
-
238
- /**
239
- * Return a promise representing the generator run process
240
- * @return {Promise} Promise resolved on end or rejected on error
241
- */
242
- RunContext.prototype.toPromise = function () {
243
- if (this.settings.runEnvironment) {
244
- throw new Error('RunContext with runEnvironment uses promises by default');
245
- }
246
-
247
- return new Promise(
248
- function (resolve, reject) {
249
- this.on(
250
- 'end',
251
- function () {
252
- resolve(new RunResult(this._createRunResultOptions()));
253
- }.bind(this)
254
- );
255
- this.on('error', reject);
256
- }.bind(this)
257
- );
258
- };
259
-
260
- /**
261
- * Promise `.then()` duck typing
262
- * @return {Promise}
263
- */
264
- RunContext.prototype.then = function () {
265
- const promise = this.toPromise();
266
- return promise.then(...arguments);
267
- };
268
-
269
- /**
270
- * Promise `.catch()` duck typing
271
- * @return {Promise}
272
- */
273
- RunContext.prototype.catch = function () {
274
- const promise = this.toPromise();
275
- return promise.catch(...arguments);
276
- };
277
-
278
- /**
279
- * Set the target directory.
280
- * @private
281
- * @param {String} dirPath - Directory path (relative to CWD). Prefer passing an absolute
282
- * file path for predictable results
283
- * @return {this} run context instance
284
- */
285
-
286
- RunContext.prototype.setDir = function (dirPath, tmpdir) {
287
- if (this.inDirSet) {
288
- this.completed = true;
289
- throw new Error('Test directory has already been set.');
290
- }
291
-
292
- if (tmpdir !== undefined) {
293
- this.settings.tmpdir = tmpdir;
294
- }
295
-
296
- this.oldCwd = this.oldCwd || process.cwd();
297
-
298
- this.inDirSet = true;
299
- this.targetDirectory = dirPath;
300
- return this;
301
- };
302
-
303
- /**
304
- * Clean the provided directory, then change directory into it
305
- * @param {String} dirPath - Directory path (relative to CWD). Prefer passing an absolute
306
- * file path for predictable results
307
- * @param {Function} [cb] - callback who'll receive the folder path as argument
308
- * @return {this} run context instance
309
- */
310
-
311
- RunContext.prototype.inDir = function (dirPath, cb) {
312
- this.setDir(dirPath, true);
313
- this.helpers.testDirectory(
314
- dirPath,
315
- (cb || (() => {})).bind(this, path.resolve(dirPath))
316
- );
317
- return this;
318
- };
319
-
320
- /**
321
- * Register an callback to prepare the destination folder.
322
- * @param {Function} [cb] - callback who'll receive the folder path as argument
323
- * @return {this} run context instance
324
- */
325
-
326
- RunContext.prototype.doInDir = function (cb) {
327
- this.inDirCallbacks.push(cb);
328
- return this;
329
- };
330
-
331
- /**
332
- * Change directory without deleting directory content.
333
- * @param {String} dirPath - Directory path (relative to CWD). Prefer passing an absolute
334
- * file path for predictable results
335
- * @return {this} run context instance
336
- */
337
- RunContext.prototype.cd = function (dirPath) {
338
- dirPath = path.resolve(dirPath);
339
- this.setDir(dirPath, false);
340
- try {
341
- process.chdir(dirPath);
342
- } catch (error) {
343
- this.completed = true;
344
- throw new Error(error.message + ' ' + dirPath);
345
- }
346
-
347
- return this;
348
- };
349
-
350
- /**
351
- * Creates a temporary directory and change the CWD into it
352
- *
353
- * This method is called automatically when creating a RunContext. Only use it if you need
354
- * to use the callback.
355
- *
356
- * @param {Function} [cb] - callback who'll receive the folder path as argument
357
- * @return {this} run context instance
358
- */
359
- RunContext.prototype.inTmpDir = function (cb) {
360
- return this.inDir(
361
- path.join(tempDirectory, crypto.randomBytes(20).toString('hex')),
362
- cb
363
- );
364
- };
365
-
366
- /**
367
- * Restore cwd to initial cwd.
368
- * @return {this} run context instance
369
- */
370
-
371
- RunContext.prototype.restore = function () {
372
- if (this.oldCwd) {
373
- process.chdir(this.oldCwd);
374
- }
375
-
376
- return this;
377
- };
378
-
379
- /**
380
- * Clean the directory used for tests inside inDir/inTmpDir
381
- * @param {Boolean} force - force directory cleanup for not tmpdir
382
- */
383
- RunContext.prototype.cleanup = function () {
384
- this.restore();
385
- if (this.settings.tmpdir !== false) {
386
- this.cleanTestDirectory();
387
- }
388
- };
389
-
390
- /**
391
- * Create an environment
392
- *
393
- * This method is called automatically when creating a RunContext. Only use it if you need
394
- * to use the callback.
395
- *
396
- * @param {Function} [cb] - callback who'll receive the folder path as argument
397
- * @return {this} run context instance
398
- */
399
- RunContext.prototype.withEnvironment = function (cb) {
400
- this.envCB = cb;
401
- return this;
402
- };
403
-
404
- /**
405
- * Run lookup on the environment.
406
- *
407
- * @param {Object|Array} [lookups] - lookup to run.
408
- * @return {this} run context instance.
409
- */
410
- RunContext.prototype.withLookups = function (lookups) {
411
- lookups = Array.isArray(lookups) ? lookups : [lookups];
412
- this.lookups = this.lookups.concat(lookups);
413
- return this;
414
- };
415
-
416
- /**
417
- * Clean the directory used for tests inside inDir/inTmpDir
418
- * @param {Boolean} force - force directory cleanup for not tmpdir
419
- */
420
- RunContext.prototype.cleanTestDirectory = function (force = false) {
421
- if (!force && this.settings.tmpdir === false) {
422
- throw new Error('Cleanup test dir called with false tmpdir option.');
423
- }
424
-
425
- if (this.targetDirectory && fs.existsSync(this.targetDirectory)) {
426
- fs.rmdirSync(this.targetDirectory, {recursive: true});
427
- }
428
- };
429
-
430
- /**
431
- * Provide arguments to the run context
432
- * @param {String|Array} args - command line arguments as Array or space separated string
433
- * @return {this}
434
- */
435
-
436
- RunContext.prototype.withArguments = function (args) {
437
- const argsArray = typeof args === 'string' ? args.split(' ') : args;
438
- assert(
439
- Array.isArray(argsArray),
440
- 'args should be either a string separated by spaces or an array'
441
- );
442
- this.args = this.args.concat(argsArray);
443
- return this;
444
- };
445
-
446
- /**
447
- * Provide options to the run context
448
- * @param {Object} options - command line options (e.g. `--opt-one=foo`)
449
- * @return {this}
450
- */
451
-
452
- RunContext.prototype.withOptions = function (options) {
453
- if (!options) {
454
- return this;
455
- }
456
-
457
- // Add options as both kebab and camel case. This is to stay backward compatibles with
458
- // the switch we made to meow for options parsing.
459
- Object.keys(options).forEach(function (key) {
460
- options[_.camelCase(key)] = options[key];
461
- options[_.kebabCase(key)] = options[key];
462
- });
463
-
464
- this.options = {...this.options, ...options};
465
- return this;
466
- };
467
-
468
- /**
469
- * Mock the prompt with dummy answers
470
- * @param {Object} answers - Answers to the prompt questions
471
- * @param {Object|Function} [options] - Options or callback.
472
- * @param {Function} [options.callback] - Callback.
473
- * @param {Boolean} [options.throwOnMissingAnswer] - Throw if a answer is missing.
474
- * @return {this}
475
- */
476
-
477
- RunContext.prototype.withPrompts = function (answers, options) {
478
- this.answers = {...this.answers, ...answers};
479
- this.promptOptions = options;
480
- return this;
481
- };
482
-
483
- /**
484
- * Provide dependent generators
485
- * @param {Array} dependencies - paths to the generators dependencies
486
- * @return {this}
487
- * @example
488
- * var angular = new RunContext('../../app');
489
- * angular.withGenerators([
490
- * '../../common',
491
- * '../../controller',
492
- * '../../main',
493
- * [helpers.createDummyGenerator(), 'testacular:app']
494
- * ]);
495
- * angular.on('end', function () {
496
- * // assert something
497
- * });
498
- */
499
-
500
- RunContext.prototype.withGenerators = function (dependencies) {
501
- assert(Array.isArray(dependencies), 'dependencies should be an array');
502
- this.dependencies = this.dependencies.concat(dependencies);
503
- return this;
504
- };
505
-
506
- /**
507
- * Create mocked generators
508
- * @param {Array} namespaces - namespaces of mocked generators
509
- * @return {this}
510
- * @example
511
- * var angular = helpers
512
- * .create('../../app')
513
- * .withMockedGenerators([
514
- * 'foo:app',
515
- * 'foo:bar',
516
- * ])
517
- * .run()
518
- * .then(runResult => assert(runResult
519
- * .mockedGenerators['foo:app']
520
- .calledOnce));
521
- */
522
-
523
- RunContext.prototype.withMockedGenerators = function (namespaces) {
524
- assert(Array.isArray(namespaces), 'namespaces should be an array');
525
- const entries = namespaces.map((namespace) => [
526
- namespace,
527
- this.helpers.createMockedGenerator()
528
- ]);
529
- this.mockedGenerators = {
530
- ...this.mockedGenerators,
531
- ...Object.fromEntries(entries)
532
- };
533
- const dependencies = entries.map(([namespace, generator]) => [
534
- generator,
535
- namespace
536
- ]);
537
- this.dependencies = this.dependencies.concat(dependencies);
538
- return this;
539
- };
540
-
541
- /**
542
- * Mock the local configuration with the provided config
543
- * @param {Object} localConfig - should look just like if called config.getAll()
544
- * @return {this}
545
- */
546
- RunContext.prototype.withLocalConfig = function (localConfig) {
547
- assert(typeof localConfig === 'object', 'config should be an object');
548
- this.localConfig = localConfig;
549
- return this;
550
- };
551
-
552
- module.exports = RunContext;