alchemymvc 1.4.0 → 1.4.1

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.
Files changed (37) hide show
  1. package/lib/app/behaviour/revision_behaviour.js +1 -1
  2. package/lib/app/behaviour/sluggable_behaviour.js +2 -2
  3. package/lib/app/datasource/mongo_datasource.js +19 -3
  4. package/lib/app/helper/cron.js +2 -2
  5. package/lib/app/helper_datasource/00-nosql_datasource.js +9 -3
  6. package/lib/app/helper_datasource/05-fallback_datasource.js +2 -0
  7. package/lib/app/helper_datasource/idb_datasource.js +7 -5
  8. package/lib/app/helper_datasource/remote_datasource.js +1 -1
  9. package/lib/app/helper_field/password_field.js +4 -2
  10. package/lib/app/helper_field/schema_field.js +3 -2
  11. package/lib/app/helper_model/00-base_criteria.js +14 -0
  12. package/lib/app/helper_model/05-criteria_expressions.js +30 -7
  13. package/lib/app/helper_model/10-model_criteria.js +47 -8
  14. package/lib/app/helper_model/document.js +11 -2
  15. package/lib/app/helper_model/model.js +6 -3
  16. package/lib/class/conduit.js +5 -2
  17. package/lib/class/controller.js +1 -0
  18. package/lib/class/document.js +39 -11
  19. package/lib/class/import_stream_parser.js +299 -0
  20. package/lib/class/migration.js +5 -2
  21. package/lib/class/model.js +10 -140
  22. package/lib/class/plugin.js +32 -3
  23. package/lib/class/router.js +24 -27
  24. package/lib/class/schema_client.js +38 -7
  25. package/lib/class/sitemap.js +2 -2
  26. package/lib/core/alchemy.js +110 -162
  27. package/lib/core/alchemy_load_functions.js +64 -5
  28. package/lib/core/base.js +2 -2
  29. package/lib/core/middleware.js +31 -5
  30. package/lib/core/setting.js +12 -9
  31. package/lib/scripts/create_constants.js +5 -1
  32. package/lib/stages/00-load_core.js +8 -2
  33. package/lib/testing/browser.js +1164 -0
  34. package/lib/testing/harness.js +840 -0
  35. package/package.json +10 -3
  36. package/testing/browser.js +27 -0
  37. package/testing.js +37 -0
@@ -0,0 +1,840 @@
1
+ /**
2
+ * Test Harness for Alchemy projects
3
+ *
4
+ * This module provides a reusable test harness for AlchemyMVC applications and plugins.
5
+ * It handles all the boilerplate setup including:
6
+ * - Environment variables
7
+ * - MongoDB in-memory database (mongo-unit)
8
+ * - Alchemy server startup
9
+ * - Plugin loading
10
+ * - Cleanup/teardown
11
+ *
12
+ * Usage:
13
+ * const TestHarness = require('alchemymvc/testing');
14
+ * const harness = new TestHarness({ path_root: __dirname });
15
+ * await harness.start();
16
+ * // ... run tests ...
17
+ * await harness.stop();
18
+ *
19
+ * @author Jelle De Loecker <jelle@elevenways.be>
20
+ * @since 1.4.1
21
+ * @version 1.4.1
22
+ */
23
+ 'use strict';
24
+
25
+ const libpath = require('path');
26
+
27
+ /**
28
+ * The TestHarness class
29
+ *
30
+ * @author Jelle De Loecker <jelle@elevenways.be>
31
+ * @since 1.4.1
32
+ * @version 1.4.1
33
+ *
34
+ * @param {Object} options
35
+ */
36
+ const TestHarness = Function.inherits('Informer', 'Alchemy.Testing', function TestHarness(options) {
37
+
38
+ TestHarness.super.call(this);
39
+
40
+ if (options == null) {
41
+ options = {};
42
+ }
43
+
44
+ // Store options with defaults
45
+ this.options = Object.assign({
46
+ // Required: path to the project root (where app/ is)
47
+ path_root: null,
48
+
49
+ // Environment to use (default: 'test')
50
+ environment: 'test',
51
+
52
+ // Skip loading local.js config (default: true for tests)
53
+ skip_local_config: true,
54
+
55
+ // Use mongo-unit for in-memory MongoDB (default: true)
56
+ use_mongo_unit: true,
57
+
58
+ // Server port (default: random between 3470-3570)
59
+ port: 3470 + Math.floor(Math.random() * 100),
60
+
61
+ // Silent mode - suppress Alchemy logs (default: true)
62
+ silent: true,
63
+
64
+ // Plugins to load: { name: options }
65
+ plugins: {},
66
+
67
+ // Extra stage tasks to run
68
+ stage_tasks: {},
69
+
70
+ // Disable request postponing on overload (default: true for tests)
71
+ disable_postpone: true,
72
+
73
+ }, options);
74
+
75
+ // Validate required options
76
+ if (!this.options.path_root) {
77
+ throw new Error('TestHarness requires path_root option');
78
+ }
79
+
80
+ // State tracking
81
+ this._mongo_uri = null;
82
+ this._mongo_unit = null;
83
+ this._started = false;
84
+ this._alchemy_required = false;
85
+
86
+ // Store reference to Puppeteer browser if used
87
+ this.browser = null;
88
+ this.page = null;
89
+ });
90
+
91
+ /**
92
+ * Get the MongoUnit module (lazy loaded)
93
+ *
94
+ * @author Jelle De Loecker <jelle@elevenways.be>
95
+ * @since 1.4.1
96
+ * @version 1.4.1
97
+ *
98
+ * @return {Object}
99
+ */
100
+ TestHarness.setMethod(function getMongoUnit() {
101
+
102
+ if (!this._mongo_unit) {
103
+ try {
104
+ this._mongo_unit = require('mongo-unit');
105
+ } catch (err) {
106
+ throw new Error('mongo-unit is required for testing. Install it with: npm install --save-dev mongo-unit');
107
+ }
108
+ }
109
+
110
+ return this._mongo_unit;
111
+ });
112
+
113
+ /**
114
+ * Resolve a plugin path from node_modules
115
+ *
116
+ * This method mimics the logic in alchemy.usePlugin() to find plugins.
117
+ * It searches in multiple locations and handles the "alchemy-" prefix.
118
+ *
119
+ * @author Jelle De Loecker <jelle@elevenways.be>
120
+ * @since 1.4.1
121
+ * @version 1.4.1
122
+ *
123
+ * @param {string} plugin_name
124
+ *
125
+ * @return {string}
126
+ */
127
+ TestHarness.setMethod(function getPluginPath(plugin_name) {
128
+
129
+ // If already an absolute path, return as-is
130
+ if (libpath.isAbsolute(plugin_name)) {
131
+ return plugin_name;
132
+ }
133
+
134
+ // Determine names to try (with and without alchemy- prefix)
135
+ let names_to_try;
136
+ if (plugin_name.startsWith('alchemy-')) {
137
+ names_to_try = [plugin_name, plugin_name.slice(8)];
138
+ } else {
139
+ names_to_try = ['alchemy-' + plugin_name, plugin_name];
140
+ }
141
+
142
+ // Build search paths: path_root and its parent directories
143
+ // This handles the case where tests run from test_root inside a plugin
144
+ let search_paths = [this.options.path_root];
145
+ let current = this.options.path_root;
146
+
147
+ for (let i = 0; i < 5; i++) {
148
+ let parent = libpath.dirname(current);
149
+ if (parent === current) break;
150
+ search_paths.push(parent);
151
+ current = parent;
152
+ }
153
+
154
+ // Use require.resolve - Node's optimized module resolution
155
+ // It handles symlinks, node_modules lookup, and caching
156
+ for (let name of names_to_try) {
157
+ try {
158
+ let resolved = require.resolve(name + '/package.json', { paths: search_paths });
159
+ return libpath.dirname(resolved);
160
+ } catch (err) {
161
+ // Not found, try next name
162
+ }
163
+ }
164
+
165
+ throw new Error(`Could not find plugin "${plugin_name}" in search paths starting from: ${this.options.path_root}`);
166
+ });
167
+
168
+ /**
169
+ * Set up environment variables
170
+ * This MUST be called before requiring alchemymvc
171
+ *
172
+ * @author Jelle De Loecker <jelle@elevenways.be>
173
+ * @since 1.4.1
174
+ * @version 1.4.1
175
+ */
176
+ TestHarness.setMethod(function setupEnvironment() {
177
+
178
+ // Disable Janeway terminal UI
179
+ process.env.DISABLE_JANEWAY = '1';
180
+
181
+ // Suppress "file not found" warnings during loading
182
+ // This helps keep test output clean, but actual code errors in files
183
+ // will still be surfaced because the load stage handles ENOENT separately.
184
+ process.env.NO_ALCHEMY_LOAD_WARNING = '1';
185
+
186
+ // Set PATH_ROOT before requiring alchemymvc
187
+ process.env.PATH_ROOT = this.options.path_root;
188
+
189
+ // Skip local.js if requested
190
+ if (this.options.skip_local_config) {
191
+ process.env.ALCHEMY_SKIP_LOCAL_CONFIG = '1';
192
+ }
193
+
194
+ // Set environment
195
+ if (this.options.environment) {
196
+ process.env.ALCHEMY_ENV = this.options.environment;
197
+ }
198
+ });
199
+
200
+ /**
201
+ * Require AlchemyMVC (only once)
202
+ *
203
+ * @author Jelle De Loecker <jelle@elevenways.be>
204
+ * @since 1.4.1
205
+ * @version 1.4.1
206
+ */
207
+ TestHarness.setMethod(function requireAlchemy() {
208
+
209
+ if (this._alchemy_required) {
210
+ return this._alchemy_required;
211
+ }
212
+
213
+ let pledge = this._alchemy_required = new Pledge();
214
+
215
+ // Set up environment BEFORE requiring
216
+ this.setupEnvironment();
217
+
218
+ // Require alchemymvc - use the bootstrap from the same package
219
+ const ROOT_STAGE = require('../bootstrap.js');
220
+
221
+ ROOT_STAGE.getStage('load_core').addPostTask(() => pledge.resolve());
222
+
223
+ return pledge;
224
+ });
225
+
226
+ /**
227
+ * Start the in-memory MongoDB
228
+ *
229
+ * @author Jelle De Loecker <jelle@elevenways.be>
230
+ * @since 1.4.1
231
+ * @version 1.4.1
232
+ *
233
+ * @return {Promise<string>} MongoDB URI
234
+ */
235
+ TestHarness.setMethod(async function startMongo() {
236
+
237
+ if (!this.options.use_mongo_unit) {
238
+ return null;
239
+ }
240
+
241
+ if (this._mongo_uri) {
242
+ return this._mongo_uri;
243
+ }
244
+
245
+ let MongoUnit = this.getMongoUnit();
246
+ this._mongo_uri = await MongoUnit.start({ verbose: false });
247
+
248
+ if (!this._mongo_uri) {
249
+ throw new Error('Failed to start mongo-unit');
250
+ }
251
+
252
+ return this._mongo_uri;
253
+ });
254
+
255
+ /**
256
+ * Configure and start the Alchemy server
257
+ *
258
+ * @author Jelle De Loecker <jelle@elevenways.be>
259
+ * @since 1.4.1
260
+ * @version 1.4.1
261
+ *
262
+ * @return {Promise}
263
+ */
264
+ TestHarness.setMethod(function startServer() {
265
+
266
+ return new Promise((resolve, reject) => {
267
+
268
+ // Configure network settings
269
+ alchemy.setSetting('network.port', this.options.port);
270
+
271
+ // Disable request postponing for tests
272
+ if (this.options.disable_postpone) {
273
+ alchemy.setSetting('performance.postpone_requests_on_overload', false);
274
+ }
275
+
276
+ // Configure datasource if using mongo-unit
277
+ if (this._mongo_uri) {
278
+ STAGES.getStage('datasource').addPostTask(() => {
279
+ Datasource.create('mongo', 'default', { uri: this._mongo_uri });
280
+ });
281
+ }
282
+
283
+ // Register additional module search paths
284
+ // This allows plugins to find their own dependencies during tests
285
+ if (this.options.extra_module_paths) {
286
+ for (let extra_path of this.options.extra_module_paths) {
287
+ if (!libpath.isAbsolute(extra_path)) {
288
+ extra_path = libpath.resolve(this.options.path_root, extra_path);
289
+ }
290
+ alchemy.addModuleSearchPath(extra_path);
291
+ }
292
+ }
293
+
294
+ // Load plugins
295
+ for (let [name, options] of Object.entries(this.options.plugins)) {
296
+
297
+ let plugin_options = { ...options };
298
+
299
+ // Resolve path if not already absolute
300
+ if (!plugin_options.path_to_plugin) {
301
+ plugin_options.path_to_plugin = this.getPluginPath(name);
302
+ } else if (!libpath.isAbsolute(plugin_options.path_to_plugin)) {
303
+ plugin_options.path_to_plugin = libpath.resolve(
304
+ this.options.path_root,
305
+ plugin_options.path_to_plugin
306
+ );
307
+ }
308
+
309
+ // Add the plugin's node_modules to the module search paths
310
+ // This allows dependencies of the plugin to be found during tests
311
+ let plugin_node_modules = libpath.resolve(plugin_options.path_to_plugin, 'node_modules');
312
+ alchemy.addModuleSearchPath(plugin_node_modules);
313
+
314
+ alchemy.usePlugin(name, plugin_options);
315
+ }
316
+
317
+ // Add custom stage tasks
318
+ for (let [stage_name, tasks] of Object.entries(this.options.stage_tasks)) {
319
+ let stage = STAGES.getStage(stage_name);
320
+
321
+ if (!stage) {
322
+ console.warn(`TestHarness: Stage '${stage_name}' not found`);
323
+ continue;
324
+ }
325
+
326
+ if (tasks.pre) {
327
+ for (let task of Array.isArray(tasks.pre) ? tasks.pre : [tasks.pre]) {
328
+ stage.addPreTask(task);
329
+ }
330
+ }
331
+
332
+ if (tasks.post) {
333
+ for (let task of Array.isArray(tasks.post) ? tasks.post : [tasks.post]) {
334
+ stage.addPostTask(task);
335
+ }
336
+ }
337
+ }
338
+
339
+ // Start the server
340
+ alchemy.start({ silent: this.options.silent }, (err) => {
341
+ if (err) {
342
+ reject(err);
343
+ } else {
344
+ this._started = true;
345
+ resolve();
346
+ }
347
+ });
348
+ });
349
+ });
350
+
351
+ /**
352
+ * Start everything: mongo-unit, require alchemy, start server
353
+ * This is the main entry point for setting up tests
354
+ *
355
+ * @author Jelle De Loecker <jelle@elevenways.be>
356
+ * @since 1.4.1
357
+ * @version 1.4.1
358
+ *
359
+ * @return {Promise}
360
+ */
361
+ TestHarness.setMethod(async function start() {
362
+
363
+ // Start mongo first (if enabled)
364
+ await this.startMongo();
365
+
366
+ // Require Alchemy (sets up environment vars first)
367
+ await this.requireAlchemy();
368
+
369
+ // Start the server
370
+ await this.startServer();
371
+
372
+ return this;
373
+ });
374
+
375
+ /**
376
+ * Stop everything: mongo-unit, alchemy server, browser
377
+ *
378
+ * @author Jelle De Loecker <jelle@elevenways.be>
379
+ * @since 1.4.1
380
+ * @version 1.4.1
381
+ *
382
+ * @return {Promise}
383
+ */
384
+ TestHarness.setMethod(async function stop() {
385
+
386
+ // Close browser if open
387
+ if (this.browser) {
388
+ await this.browser.close();
389
+ this.browser = null;
390
+ this.page = null;
391
+ }
392
+
393
+ // Stop mongo-unit
394
+ if (this._mongo_unit && this._mongo_uri) {
395
+ this._mongo_unit.stop();
396
+ this._mongo_uri = null;
397
+ }
398
+
399
+ // Stop alchemy
400
+ if (this._started && typeof alchemy !== 'undefined') {
401
+ alchemy.stop();
402
+ this._started = false;
403
+ }
404
+ });
405
+
406
+ /**
407
+ * Get a full URL for a path
408
+ *
409
+ * @author Jelle De Loecker <jelle@elevenways.be>
410
+ * @since 1.4.1
411
+ * @version 1.4.1
412
+ *
413
+ * @param {string} path
414
+ *
415
+ * @return {string}
416
+ */
417
+ TestHarness.setMethod(function getUrl(path) {
418
+
419
+ if (!path.startsWith('/')) {
420
+ path = '/' + path;
421
+ }
422
+
423
+ return 'http://localhost:' + this.options.port + path;
424
+ });
425
+
426
+ /**
427
+ * Get a URL for a named route
428
+ *
429
+ * @author Jelle De Loecker <jelle@elevenways.be>
430
+ * @since 1.4.1
431
+ * @version 1.4.1
432
+ *
433
+ * @param {string} route_name
434
+ * @param {Object} params
435
+ *
436
+ * @return {string}
437
+ */
438
+ TestHarness.setMethod(function getRouteUrl(route_name, params) {
439
+
440
+ let url = Router.getUrl(route_name, params);
441
+
442
+ url.host = 'localhost';
443
+ url.protocol = 'http';
444
+ url.port = this.options.port;
445
+
446
+ return String(url);
447
+ });
448
+
449
+ /**
450
+ * Make an HTTP request using Blast.fetch
451
+ *
452
+ * By default on the server, Blast.fetch doesn't undry JSON-dry responses.
453
+ * This method sets `allow_json_dry_response: true` to mimic browser behavior,
454
+ * since Alchemy sends `application/json-dry` responses by default.
455
+ *
456
+ * @author Jelle De Loecker <jelle@elevenways.be>
457
+ * @since 1.4.1
458
+ * @version 1.4.1
459
+ *
460
+ * @param {string} path_or_url
461
+ * @param {Object} options
462
+ *
463
+ * @return {Promise<{response: Object, body: *}>}
464
+ */
465
+ TestHarness.setMethod(function fetch(path_or_url, options) {
466
+
467
+ if (options == null) {
468
+ options = {};
469
+ }
470
+
471
+ // Enable JSON-dry response handling (like browser does)
472
+ // This makes the test harness behave like a browser client
473
+ if (options.allow_json_dry_response == null) {
474
+ options.allow_json_dry_response = true;
475
+ }
476
+
477
+ return new Promise((resolve, reject) => {
478
+
479
+ let url;
480
+
481
+ if (path_or_url.startsWith('http')) {
482
+ url = path_or_url;
483
+ } else {
484
+ url = this.getUrl(path_or_url);
485
+ }
486
+
487
+ Blast.fetch(url, options, (err, res, body) => {
488
+ if (err) {
489
+ reject(err);
490
+ } else {
491
+ resolve({ response: res, body });
492
+ }
493
+ });
494
+ });
495
+ });
496
+
497
+ /**
498
+ * Make an HTTP request to a named route
499
+ *
500
+ * @author Jelle De Loecker <jelle@elevenways.be>
501
+ * @since 1.4.1
502
+ * @version 1.4.1
503
+ *
504
+ * @param {string} route_name
505
+ * @param {Object} route_params
506
+ * @param {Object} fetch_options
507
+ *
508
+ * @return {Promise<{response: Object, body: string}>}
509
+ */
510
+ TestHarness.setMethod(function fetchRoute(route_name, route_params, fetch_options) {
511
+
512
+ if (route_params == null) {
513
+ route_params = {};
514
+ }
515
+
516
+ if (fetch_options == null) {
517
+ fetch_options = {};
518
+ }
519
+
520
+ let url = this.getRouteUrl(route_name, route_params);
521
+ return this.fetch(url, fetch_options);
522
+ });
523
+
524
+ /**
525
+ * Load Puppeteer browser for UI testing
526
+ *
527
+ * @author Jelle De Loecker <jelle@elevenways.be>
528
+ * @since 1.4.1
529
+ * @version 1.4.1
530
+ *
531
+ * @param {Object} options
532
+ *
533
+ * @return {Promise<{browser: Object, page: Object}>}
534
+ */
535
+ TestHarness.setMethod(async function loadBrowser(options) {
536
+
537
+ if (options == null) {
538
+ options = {};
539
+ }
540
+
541
+ if (this.browser) {
542
+ return { browser: this.browser, page: this.page };
543
+ }
544
+
545
+ let puppeteer;
546
+
547
+ try {
548
+ puppeteer = require('puppeteer');
549
+ } catch (err) {
550
+ throw new Error('puppeteer is required for browser testing. Install it with: npm install --save-dev puppeteer');
551
+ }
552
+
553
+ let launch_options = {
554
+ headless: 'new',
555
+ ...options,
556
+ };
557
+
558
+ this.browser = await puppeteer.launch(launch_options);
559
+ this.page = await this.browser.newPage();
560
+
561
+ return { browser: this.browser, page: this.page };
562
+ });
563
+
564
+ /**
565
+ * Navigate to a path in the browser
566
+ *
567
+ * @author Jelle De Loecker <jelle@elevenways.be>
568
+ * @since 1.4.1
569
+ * @version 1.4.1
570
+ *
571
+ * @param {string} path
572
+ *
573
+ * @return {Promise}
574
+ */
575
+ TestHarness.setMethod(async function goto(path) {
576
+
577
+ if (!this.page) {
578
+ await this.loadBrowser();
579
+ }
580
+
581
+ // Expose default static variables for each navigation
582
+ if (typeof alchemy !== 'undefined') {
583
+ alchemy.exposeDefaultStaticVariables();
584
+ }
585
+
586
+ let url = this.getUrl(path);
587
+ let resource = await this.page.goto(url);
588
+ let status = await resource.status();
589
+
590
+ if (status >= 400) {
591
+ throw new Error('Received a ' + status + ' error response for "' + path + '"');
592
+ }
593
+
594
+ return resource;
595
+ });
596
+
597
+ /**
598
+ * Evaluate code in the browser page context
599
+ *
600
+ * @author Jelle De Loecker <jelle@elevenways.be>
601
+ * @since 1.4.1
602
+ * @version 1.4.1
603
+ *
604
+ * @param {Function} fn
605
+ * @param {...*} args
606
+ *
607
+ * @return {Promise<*>}
608
+ */
609
+ TestHarness.setMethod(function evaluate(fn, ...args) {
610
+
611
+ if (!this.page) {
612
+ throw new Error('Browser not loaded. Call loadBrowser() first.');
613
+ }
614
+
615
+ return this.page.evaluate(fn, ...args);
616
+ });
617
+
618
+ /**
619
+ * Click an element in the browser
620
+ *
621
+ * @author Jelle De Loecker <jelle@elevenways.be>
622
+ * @since 1.4.1
623
+ * @version 1.4.1
624
+ *
625
+ * @param {string} selector
626
+ *
627
+ * @return {Promise<boolean>}
628
+ */
629
+ TestHarness.setMethod(async function click(selector) {
630
+
631
+ return this.evaluate((sel) => {
632
+ let element = document.querySelector(sel);
633
+
634
+ if (!element) {
635
+ return false;
636
+ }
637
+
638
+ element.click();
639
+ return true;
640
+ }, selector);
641
+ });
642
+
643
+ /**
644
+ * Get an element's data from the browser
645
+ *
646
+ * @author Jelle De Loecker <jelle@elevenways.be>
647
+ * @since 1.4.1
648
+ * @version 1.4.1
649
+ *
650
+ * @param {string} selector
651
+ *
652
+ * @return {Promise<Object|false>}
653
+ */
654
+ TestHarness.setMethod(async function queryElement(selector) {
655
+
656
+ return this.evaluate((sel) => {
657
+ let element = document.querySelector(sel);
658
+
659
+ if (!element) {
660
+ return false;
661
+ }
662
+
663
+ return {
664
+ html: element.outerHTML,
665
+ text: element.textContent,
666
+ location: document.location.pathname,
667
+ };
668
+ }, selector);
669
+ });
670
+
671
+ /**
672
+ * Set a value on a form element
673
+ *
674
+ * @author Jelle De Loecker <jelle@elevenways.be>
675
+ * @since 1.4.1
676
+ * @version 1.4.1
677
+ *
678
+ * @param {string} selector
679
+ * @param {*} value
680
+ *
681
+ * @return {Promise<Object|false>}
682
+ */
683
+ TestHarness.setMethod(async function setValue(selector, value) {
684
+
685
+ return this.evaluate((sel, val) => {
686
+ let element = document.querySelector(sel);
687
+
688
+ if (!element) {
689
+ return false;
690
+ }
691
+
692
+ element.value = val;
693
+
694
+ return {
695
+ html: element.outerHTML,
696
+ value: element.value,
697
+ };
698
+ }, selector, value);
699
+ });
700
+
701
+ /**
702
+ * Create a Mocha describe block for harness setup
703
+ * This is a convenience method for the most common test pattern
704
+ *
705
+ * @author Jelle De Loecker <jelle@elevenways.be>
706
+ * @since 1.4.1
707
+ * @version 1.4.1
708
+ *
709
+ * @param {Function} describe Mocha describe function
710
+ * @param {Function} it Mocha it function
711
+ *
712
+ * @return {TestHarness}
713
+ */
714
+ TestHarness.setMethod(function describeSetup(describe, it) {
715
+
716
+ let that = this;
717
+
718
+ describe('Test Setup', function() {
719
+ this.timeout(150000);
720
+
721
+ if (that.options.use_mongo_unit) {
722
+ it('should start in-memory MongoDB', async function() {
723
+ await that.startMongo();
724
+ });
725
+ }
726
+
727
+ it('should start the Alchemy server', async function() {
728
+ that.requireAlchemy();
729
+ await that.startServer();
730
+ });
731
+ });
732
+
733
+ return this;
734
+ });
735
+
736
+ /**
737
+ * Create a Mocha describe block for harness teardown
738
+ *
739
+ * @author Jelle De Loecker <jelle@elevenways.be>
740
+ * @since 1.4.1
741
+ * @version 1.4.1
742
+ *
743
+ * @param {Function} describe Mocha describe function
744
+ * @param {Function} it Mocha it function
745
+ *
746
+ * @return {TestHarness}
747
+ */
748
+ TestHarness.setMethod(function describeTeardown(describe, it) {
749
+
750
+ let that = this;
751
+
752
+ describe('Teardown', function() {
753
+ it('should stop all services', async function() {
754
+ await that.stop();
755
+ });
756
+ });
757
+
758
+ return this;
759
+ });
760
+
761
+ // =============================================================================
762
+ // Utility Methods
763
+ // =============================================================================
764
+
765
+ /**
766
+ * Clean up whitespace in text (trim, normalize newlines and spaces)
767
+ *
768
+ * @author Jelle De Loecker <jelle@elevenways.be>
769
+ * @since 1.4.1
770
+ * @version 1.4.1
771
+ *
772
+ * @param {string} text
773
+ *
774
+ * @return {string}
775
+ */
776
+ TestHarness.setStatic(function despace(text) {
777
+ return text.trim().replace(/\n/g, ' ').replace(/\s\s+/g, ' ');
778
+ });
779
+
780
+ // Store original console functions
781
+ let _console_log = console.log;
782
+ let _console_error = console.error;
783
+
784
+ /**
785
+ * Silence console output (useful for testing error conditions)
786
+ *
787
+ * @author Jelle De Loecker <jelle@elevenways.be>
788
+ * @since 1.4.1
789
+ * @version 1.4.1
790
+ */
791
+ TestHarness.setMethod(function silenceConsole() {
792
+ console.log = () => {};
793
+ console.error = () => {};
794
+ });
795
+
796
+ /**
797
+ * Restore console output
798
+ *
799
+ * @author Jelle De Loecker <jelle@elevenways.be>
800
+ * @since 1.4.1
801
+ * @version 1.4.1
802
+ */
803
+ TestHarness.setMethod(function restoreConsole() {
804
+ console.log = _console_log;
805
+ console.error = _console_error;
806
+ });
807
+
808
+ /**
809
+ * Create a model dynamically during tests
810
+ * Useful for testing model features without creating test fixture files
811
+ *
812
+ * @author Jelle De Loecker <jelle@elevenways.be>
813
+ * @since 1.4.1
814
+ * @version 1.4.1
815
+ *
816
+ * @param {Function} creator Named function that sets up schema in constitute context
817
+ *
818
+ * @return {Pledge}
819
+ */
820
+ TestHarness.setMethod(function createModel(creator) {
821
+
822
+ let name = creator.name,
823
+ pledge = new Classes.Pledge();
824
+
825
+ let fnc = Function.create(name, function model(options) {
826
+ model.super.call(this, options);
827
+ });
828
+
829
+ Function.inherits('Alchemy.Model', fnc);
830
+
831
+ fnc.constitute(function() {
832
+ creator.call(this);
833
+ pledge.resolve();
834
+ });
835
+
836
+ return pledge;
837
+ });
838
+
839
+ // Export the TestHarness class
840
+ module.exports = TestHarness;