@travetto/test 3.0.0-rc.3 → 3.0.0-rc.6

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 (44) hide show
  1. package/README.md +19 -18
  2. package/{index.ts → __index__.ts} +2 -0
  3. package/package.json +19 -12
  4. package/src/assert/capture.ts +1 -0
  5. package/src/assert/check.ts +13 -7
  6. package/src/assert/util.ts +12 -12
  7. package/src/consumer/enhancer.ts +16 -24
  8. package/src/consumer/registry.ts +5 -2
  9. package/src/consumer/types/{index.ts → all.ts} +1 -1
  10. package/src/consumer/types/cumulative.ts +11 -6
  11. package/src/consumer/types/runnable.ts +8 -12
  12. package/src/consumer/types/tap-streamed.ts +92 -0
  13. package/src/consumer/types/tap.ts +25 -21
  14. package/src/consumer/types.ts +2 -2
  15. package/src/consumer/util.ts +1 -1
  16. package/src/decorator/suite.ts +3 -2
  17. package/src/decorator/test.ts +11 -2
  18. package/src/execute/console.ts +4 -3
  19. package/src/execute/executor.ts +26 -40
  20. package/src/execute/phase.ts +2 -2
  21. package/src/execute/runner.ts +15 -30
  22. package/src/execute/{types.d.ts → types.ts} +4 -4
  23. package/src/execute/util.ts +8 -8
  24. package/src/execute/watcher.ts +43 -20
  25. package/src/fixture.ts +10 -0
  26. package/src/model/common.ts +4 -0
  27. package/src/registry/suite.ts +10 -7
  28. package/src/worker/child.ts +6 -51
  29. package/src/worker/standard.ts +39 -18
  30. package/src/worker/types.ts +0 -1
  31. package/{bin/lib → support/bin}/run.ts +8 -9
  32. package/support/cli.test.ts +76 -0
  33. package/support/main.test-child.ts +32 -0
  34. package/support/main.test-direct.ts +15 -0
  35. package/support/main.test-watch.ts +8 -0
  36. package/support/transformer.annotate.ts +4 -5
  37. package/support/transformer.assert.ts +22 -20
  38. package/bin/cli-test.ts +0 -121
  39. package/bin/test-child.ts +0 -38
  40. package/bin/test-direct.ts +0 -23
  41. package/bin/test-watch.ts +0 -25
  42. package/src/consumer/types/tap-summary.ts +0 -78
  43. package/src/worker/isolated.ts +0 -19
  44. package/support/phase.reset.ts +0 -12
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <!-- This file was generated by @travetto/doc and should not be modified directly -->
2
- <!-- Please modify https://github.com/travetto/travetto/tree/main/module/test/doc.ts and execute "npx trv doc" to rebuild -->
2
+ <!-- Please modify https://github.com/travetto/travetto/tree/main/module/test/DOC.ts and execute "npx trv doc" to rebuild -->
3
3
  # Testing
4
4
  ## Declarative test framework
5
5
 
@@ -19,13 +19,13 @@ This module provides unit testing functionality that integrates with the framewo
19
19
 
20
20
  ## Definition
21
21
 
22
- A test suite is a collection of individual tests. All test suites are classes with the [@Suite](https://github.com/travetto/travetto/tree/main/module/test/src/decorator/suite.ts#L13) decorator. Tests are defined as methods on the suite class, using the [@Test](https://github.com/travetto/travetto/tree/main/module/test/src/decorator/test.ts#L11) decorator. All tests intrinsically support `async`/`await`.
22
+ A test suite is a collection of individual tests. All test suites are classes with the [@Suite](https://github.com/travetto/travetto/tree/main/module/test/src/decorator/suite.ts#L14) decorator. Tests are defined as methods on the suite class, using the [@Test](https://github.com/travetto/travetto/tree/main/module/test/src/decorator/test.ts#L20) decorator. All tests intrinsically support `async`/`await`.
23
23
 
24
24
  A simple example would be:
25
25
 
26
26
  **Code: Example Test Suite**
27
27
  ```typescript
28
- import * as assert from 'assert';
28
+ import assert from 'assert';
29
29
 
30
30
  import { Suite, Test } from '@travetto/test';
31
31
 
@@ -56,7 +56,7 @@ A common aspect of the tests themselves are the assertions that are made. [Node
56
56
 
57
57
  **Code: Example assertion for deep comparison**
58
58
  ```typescript
59
- import * as assert from 'assert';
59
+ import assert from 'assert';
60
60
 
61
61
  import { Suite, Test } from '@travetto/test';
62
62
 
@@ -73,28 +73,28 @@ class SimpleTest {
73
73
  would translate to:
74
74
 
75
75
  **Code: Transpiled test Code**
76
- ```doc/javascript
76
+ ```javascript
77
77
  "use strict";
78
78
  Object.defineProperty(exports, "__esModule", { value: true });
79
79
  const tslib_1 = require("tslib");
80
- const _check_1 = require("@travetto/test/src/assert/check");
81
- const ᚕ_decorator_1 = require("@travetto/registry/src/decorator");
82
- const assert = require("assert");
80
+ const _check_1 = tslib_1.__importStar(require("@travetto/test/src/assert/check.js"));
81
+ const Ⲑ_root_index_1 = tslib_1.__importStar(require("@travetto/manifest/src/root-index.js"));
82
+ const Ⲑ_decorator_1 = tslib_1.__importStar(require("@travetto/registry/src/decorator.js"));
83
+ const assert_1 = tslib_1.__importDefault(require("assert"));
83
84
  const test_1 = require("@travetto/test");
84
85
  let SimpleTest = class SimpleTest {
85
- static init = ᚕ_decorator_1.Register.initMeta(SimpleTest, ᚕsrc(__filename), 1887908328, { test: { hash: 102834457 } }, false, false);
86
+ static init = Ⲑ_root_index_1.RootIndex.registerFunction(SimpleTest, __filename, 1887908328, { test: { hash: 102834457 } }, false, false);
86
87
  async test() {
87
- _check_1.AssertCheck.check({ file: ᚕsrc(__filename), line: 10, text: "{ size: 20, address: { state: 'VA' } }", operator: "deepStrictEqual" }, true, { size: 20, address: { state: 'VA' } }, {});
88
+ _check_1.AssertCheck.check({ file: __filename, line: 10, text: "{ size: 20, address: { state: 'VA' } }", operator: "deepStrictEqual" }, true, { size: 20, address: { state: 'VA' } }, {});
88
89
  }
89
90
  };
90
91
  tslib_1.__decorate([
91
- (0, test_1.Test)({ lines: { start: 8, end: 11, codeStart: 10 } })
92
+ (0, test_1.Test)({ ident: "@Test()", lines: { start: 8, end: 11, codeStart: 10 } })
92
93
  ], SimpleTest.prototype, "test", null);
93
94
  SimpleTest = tslib_1.__decorate([
94
- _decorator_1.Register(),
95
- (0, test_1.Suite)({ lines: {} })
95
+ _decorator_1.Register(),
96
+ (0, test_1.Suite)({ ident: "@Suite()", lines: { start: 5, end: 12 } })
96
97
  ], SimpleTest);
97
- Object.defineProperty(exports, 'ᚕtrv', { configurable: true, value: true });
98
98
  ```
99
99
 
100
100
  This would ultimately produce the error like:
@@ -128,7 +128,7 @@ In addition to the standard operations, there is support for throwing/rejecting
128
128
 
129
129
  **Code: Throws vs Does Not Throw**
130
130
  ```typescript
131
- import * as assert from 'assert';
131
+ import assert from 'assert';
132
132
 
133
133
  import { Suite, Test } from '@travetto/test';
134
134
 
@@ -154,7 +154,7 @@ In addition to the standard operations, there is support for throwing/rejecting
154
154
 
155
155
  **Code: Rejects vs Does Not Reject**
156
156
  ```typescript
157
- import * as assert from 'assert';
157
+ import assert from 'assert';
158
158
 
159
159
  import { Suite, Test } from '@travetto/test';
160
160
 
@@ -185,7 +185,7 @@ Additionally, the `throws`/`rejects` assertions take in a secondary parameter to
185
185
 
186
186
  **Code: Example of different Error matching paradigms**
187
187
  ```typescript
188
- import * as assert from 'assert';
188
+ import assert from 'assert';
189
189
 
190
190
  import { Suite, Test } from '@travetto/test';
191
191
 
@@ -228,9 +228,10 @@ Usage: test [options] [regexes...]
228
228
  Options:
229
229
  -f, --format <format> Output format for test results (default: "tap")
230
230
  -c, --concurrency <concurrency> Number of tests to run concurrently (default: 4)
231
- -i, --isolated Isolated mode
232
231
  -m, --mode <mode> Test run mode (default: "standard")
233
232
  -h, --help display help for command
233
+
234
+ 
234
235
  ```
235
236
 
236
237
  The regexes are the patterns of tests you want to run, and all tests must be found under the `test/` folder.
@@ -4,3 +4,5 @@ export * from './src/model/suite';
4
4
  export * from './src/model/test';
5
5
  export * from './src/model/event';
6
6
  export * from './src/registry/suite';
7
+ export * from './src/fixture';
8
+ export * from './src/consumer/types';
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/test",
3
- "displayName": "Testing",
4
- "version": "3.0.0-rc.3",
3
+ "version": "3.0.0-rc.6",
5
4
  "description": "Declarative test framework",
6
5
  "keywords": [
7
6
  "unit-testing",
@@ -18,32 +17,40 @@
18
17
  "name": "Travetto Framework"
19
18
  },
20
19
  "files": [
21
- "index.ts",
20
+ "__index__.ts",
22
21
  "src",
23
- "bin",
24
22
  "support"
25
23
  ],
26
- "main": "index.ts",
24
+ "main": "__index__.ts",
27
25
  "repository": {
28
26
  "url": "https://github.com/travetto/travetto.git",
29
27
  "directory": "module/test"
30
28
  },
31
29
  "dependencies": {
32
- "@travetto/base": "^3.0.0-rc.1",
33
- "@travetto/transformer": "^3.0.0-rc.3",
34
- "@travetto/registry": "^3.0.0-rc.3",
35
- "@travetto/watch": "^3.0.0-rc.1",
36
- "@travetto/worker": "^3.0.0-rc.1",
37
- "@travetto/yaml": "^3.0.0-rc.1"
30
+ "@travetto/base": "^3.0.0-rc.4",
31
+ "@travetto/registry": "^3.0.0-rc.6",
32
+ "@travetto/terminal": "^3.0.0-rc.4",
33
+ "@travetto/worker": "^3.0.0-rc.4",
34
+ "@travetto/yaml": "^3.0.0-rc.4"
38
35
  },
39
36
  "peerDependencies": {
40
- "@travetto/cli": "^3.0.0-rc.1"
37
+ "@travetto/cli": "^3.0.0-rc.4",
38
+ "@travetto/transformer": "^3.0.0-rc.6"
41
39
  },
42
40
  "peerDependenciesMeta": {
41
+ "@travetto/transformer": {
42
+ "optional": true
43
+ },
43
44
  "@travetto/cli": {
44
45
  "optional": true
45
46
  }
46
47
  },
48
+ "travetto": {
49
+ "displayName": "Testing",
50
+ "profiles": [
51
+ "test"
52
+ ]
53
+ },
47
54
  "publishConfig": {
48
55
  "access": "public"
49
56
  }
@@ -1,4 +1,5 @@
1
1
  import { EventEmitter } from 'events';
2
+
2
3
  import { Assertion, TestConfig } from '../model/test';
3
4
 
4
5
  export interface CaptureAssert extends Partial<Assertion> {
@@ -1,7 +1,7 @@
1
- import * as assert from 'assert';
1
+ import assert from 'assert';
2
2
 
3
- import { PathUtil } from '@travetto/boot';
4
- import { Util, AppError, ClassInstance, Class } from '@travetto/base';
3
+ import { RootIndex } from '@travetto/manifest';
4
+ import { ObjectUtil, AppError, ClassInstance, Class } from '@travetto/base';
5
5
 
6
6
  import { ThrowableError, TestConfig, Assertion } from '../model/test';
7
7
  import { AssertCapture, CaptureAssert } from './capture';
@@ -30,6 +30,8 @@ export class AssertCheck {
30
30
  * @param args The arguments passed in
31
31
  */
32
32
  static check(assertion: CaptureAssert, positive: boolean, ...args: unknown[]): void {
33
+ assertion.file = RootIndex.getSourceFile(assertion.file);
34
+
33
35
  let fn = assertion.operator;
34
36
  assertion.operator = ASSERT_FN_OPERATOR[fn];
35
37
 
@@ -191,7 +193,7 @@ export class AssertCheck {
191
193
  shouldThrow: ThrowableError | undefined,
192
194
  assertion: CaptureAssert
193
195
  ): void {
194
- if (Util.isPrimitive(err)) {
196
+ if (ObjectUtil.isPrimitive(err)) {
195
197
  err = new Error(`${err}`);
196
198
  }
197
199
  if (!(err instanceof Error)) {
@@ -225,10 +227,12 @@ export class AssertCheck {
225
227
  ): void {
226
228
  let missed: Error | undefined;
227
229
 
230
+ assertion.file = RootIndex.getSourceFile(assertion.file);
231
+
228
232
  try {
229
233
  action();
230
234
  if (!positive) {
231
- if (!Util.isPrimitive(shouldThrow)) {
235
+ if (!ObjectUtil.isPrimitive(shouldThrow)) {
232
236
  shouldThrow = shouldThrow?.name;
233
237
  }
234
238
  throw (missed = new AppError(`No error thrown, but expected ${shouldThrow ?? 'an error'}`));
@@ -257,6 +261,8 @@ export class AssertCheck {
257
261
  ): Promise<void> {
258
262
  let missed: Error | undefined;
259
263
 
264
+ assertion.file = RootIndex.getSourceFile(assertion.file);
265
+
260
266
  try {
261
267
  if ('then' in action) {
262
268
  await action;
@@ -264,7 +270,7 @@ export class AssertCheck {
264
270
  await action();
265
271
  }
266
272
  if (!positive) {
267
- if (!Util.isPrimitive(shouldThrow)) {
273
+ if (!ObjectUtil.isPrimitive(shouldThrow)) {
268
274
  shouldThrow = shouldThrow?.name;
269
275
  }
270
276
  throw (missed = new AppError(`No error thrown, but expected ${shouldThrow ?? 'an error'} `));
@@ -286,7 +292,7 @@ export class AssertCheck {
286
292
  }
287
293
 
288
294
  AssertCapture.add({
289
- file: test.file.replace(`${PathUtil.cwd}/`, ''),
295
+ file: test.file,
290
296
  line,
291
297
  operator: 'throws',
292
298
  error: err,
@@ -1,7 +1,7 @@
1
- import * as util from 'util';
1
+ import util from 'util';
2
2
 
3
- import { PathUtil } from '@travetto/boot';
4
- import { Class, ClassInstance, Util } from '@travetto/base';
3
+ import { path, RootIndex } from '@travetto/manifest';
4
+ import { Class, ClassInstance, ObjectUtil } from '@travetto/base';
5
5
 
6
6
  import { TestConfig, Assertion, TestResult } from '../model/test';
7
7
  import { SuiteConfig } from '../model/suite';
@@ -22,14 +22,14 @@ export class AssertUtil {
22
22
  if (isCleanable(val)) {
23
23
  return val.toClean();
24
24
  } else if (val === null || val === undefined
25
- || (!(val instanceof RegExp) && Util.isPrimitive(val))
26
- || Util.isPlainObject(val) || Array.isArray(val)
25
+ || (!(val instanceof RegExp) && ObjectUtil.isPrimitive(val))
26
+ || ObjectUtil.isPlainObject(val) || Array.isArray(val)
27
27
  ) {
28
28
  return JSON.stringify(val);
29
29
  } else {
30
30
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
31
31
  const subV = val as (Class | ClassInstance);
32
- if (subV.ᚕid || !subV.constructor || (!subV.constructor.ᚕid && Util.isFunction(subV))) { // If a function, show name
32
+ if (subV.Ⲑid || !subV.constructor || (!subV.constructor.Ⲑid && ObjectUtil.isFunction(subV))) { // If a function, show name
33
33
  return subV.name;
34
34
  } else { // Else inspect
35
35
  return util.inspect(val, false, 1).replace(/\n/g, ' ');
@@ -41,16 +41,16 @@ export class AssertUtil {
41
41
  * Determine file location for a given error and the stack trace
42
42
  */
43
43
  static getPositionOfError(err: Error, filename: string): { file: string, line: number } {
44
- const lines = (err.stack ?? new Error().stack!)
45
- .replace(/[\\]/g, '/')
44
+ const cwd = path.cwd();
45
+ const lines = path.toPosix(err.stack ?? new Error().stack!)
46
46
  .split('\n')
47
47
  // Exclude node_modules, target self
48
- .filter(x => x.includes(PathUtil.cwd) && (!x.includes('node_modules') || x.includes('/test-support/')));
48
+ .filter(x => x.includes(cwd) && (!x.includes('node_modules') || x.includes('/support/')));
49
49
 
50
50
  let best = lines.filter(x => x.includes(filename))[0];
51
51
 
52
52
  if (!best) {
53
- [best] = lines.filter(x => x.includes(`${PathUtil.cwd}/test`));
53
+ [best] = lines.filter(x => x.includes(`${cwd}/test`));
54
54
  }
55
55
 
56
56
  if (!best) {
@@ -72,7 +72,7 @@ export class AssertUtil {
72
72
  line = -1;
73
73
  }
74
74
 
75
- const outFileParts = file.split(PathUtil.cwd.replace(/^[A-Za-z]:/, ''));
75
+ const outFileParts = file.split(cwd.replace(/^[A-Za-z]:/, ''));
76
76
 
77
77
  const outFile = outFileParts.length > 1 ? outFileParts[1].replace(/^[\/]/, '') : filename;
78
78
 
@@ -94,7 +94,7 @@ export class AssertUtil {
94
94
 
95
95
  const msg = error.message.split(/\n/)[0];
96
96
 
97
- const core = { file, classId: suite.classId, methodName };
97
+ const core = { file, classId: suite.classId, methodName, module: RootIndex.manifest.mainModule };
98
98
  const coreAll = { ...core, description: msg, lines: { start: line, end: line, codeStart: line } };
99
99
 
100
100
  const assert: Assertion = {
@@ -1,26 +1,18 @@
1
- import { ColorUtil } from '@travetto/boot';
1
+ import { GlobalTerminal } from '@travetto/terminal';
2
2
 
3
- export const COLOR_ENHANCER = {
4
- assertDescription: ColorUtil.makeColorer('white'),
5
- testDescription: ColorUtil.makeColorer('white', 'faint', 'bold'),
6
- success: ColorUtil.makeColorer('green', 'faint', 'bold'),
7
- failure: ColorUtil.makeColorer('red', 'faint', 'bold'),
8
- assertNumber: ColorUtil.makeColorer('blue', 'bold'),
9
- testNumber: ColorUtil.makeColorer('blue', 'bold'),
10
- assertFile: ColorUtil.makeColorer('cyan'),
11
- assertLine: ColorUtil.makeColorer('yellow'),
12
- objectInspect: ColorUtil.makeColorer('magenta'),
13
- suiteName: ColorUtil.makeColorer('yellow', 'faint', 'bold'),
14
- testName: ColorUtil.makeColorer('cyan', 'bold'),
15
- total: ColorUtil.makeColorer('white', 'bold')
16
- };
3
+ export const CONSOLE_ENHANCER = GlobalTerminal.palette({
4
+ assertDescription: 'lightGray',
5
+ testDescription: 'white',
6
+ success: 'green',
7
+ failure: 'red',
8
+ assertNumber: 'brightCyan',
9
+ testNumber: 'dodgerBlue',
10
+ assertFile: 'lightGreen',
11
+ assertLine: 'lightYellow',
12
+ objectInspect: 'magenta',
13
+ suiteName: 'yellow',
14
+ testName: 'cyan',
15
+ total: 'white'
16
+ });
17
17
 
18
- export type TestResultsEnhancer = typeof COLOR_ENHANCER;
19
-
20
- /**
21
- * Dummy enhancer does nothing
22
- */
23
- export const DUMMY_ENHANCER: TestResultsEnhancer = [
24
- Object.keys(COLOR_ENHANCER)
25
- .reduce<Partial<TestResultsEnhancer>>((acc, k) => (acc[k] = (x: unknown): string => `${x}`) && acc, {})
26
- ].filter((x): x is TestResultsEnhancer => !!x)[0];
18
+ export type TestResultsEnhancer = typeof CONSOLE_ENHANCER;
@@ -12,7 +12,7 @@ class $TestConsumerRegistry {
12
12
  * Manual initialization when running outside of the bootstrap process
13
13
  */
14
14
  async manualInit(): Promise<void> {
15
- await import('./types/index');
15
+ await import('./types/all.js');
16
16
  }
17
17
 
18
18
  /**
@@ -40,7 +40,10 @@ class $TestConsumerRegistry {
40
40
  * Get a consumer instance that supports summarization
41
41
  * @param consumer The consumer identifier or the actual consumer
42
42
  */
43
- getInstance(consumer: string | TestConsumer): TestConsumer {
43
+ async getInstance(consumer: string | TestConsumer): Promise<TestConsumer> {
44
+ // TODO: Fix consumer registry init
45
+ await this.manualInit();
46
+
44
47
  return typeof consumer === 'string' ?
45
48
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
46
49
  new ((this.get(consumer) ?? this.#primary) as ConcreteClass)() :
@@ -5,6 +5,6 @@ import './json';
5
5
  import './noop';
6
6
  import './runnable';
7
7
  import './summarizer';
8
- import './tap-summary';
8
+ import './tap-streamed';
9
9
  import './tap';
10
10
  import './xunit';
@@ -1,3 +1,5 @@
1
+ import fs from 'fs';
2
+
1
3
  import { Class } from '@travetto/base';
2
4
 
3
5
  import { TestConsumer } from '../types';
@@ -25,16 +27,19 @@ export class CumulativeSummaryConsumer implements TestConsumer {
25
27
  * state
26
28
  */
27
29
  summarizeSuite(test: TestResult): SuiteResult {
28
- try {
29
- // TODO: Load asynchronously
30
- require(test.file);
30
+ // Was only loading to verify existence (TODO: double-check)
31
+ if (fs.existsSync(test.file)) {
31
32
  this.#state[test.classId] = this.#state[test.classId] ?? {};
32
33
  this.#state[test.classId][test.methodName] = test.status;
33
34
  const SuiteCls = SuiteRegistry.getClasses().find(x =>
34
- x.ᚕid === test.classId
35
+ x.Ⲑid === test.classId
35
36
  )!;
36
- return this.computeTotal(SuiteCls);
37
- } catch {
37
+ if (SuiteCls) {
38
+ return this.computeTotal(SuiteCls);
39
+ } else {
40
+ return this.removeClass(test.classId);
41
+ }
42
+ } else {
38
43
  return this.removeClass(test.classId);
39
44
  }
40
45
  }
@@ -10,8 +10,8 @@ export class RunnableTestConsumer implements TestConsumer {
10
10
  /**
11
11
  * Build a runnable test consumer given a format or a full consumer
12
12
  */
13
- static get(consumer: string | TestConsumer): RunnableTestConsumer {
14
- return new RunnableTestConsumer(TestConsumerRegistry.getInstance(consumer));
13
+ static async get(consumer: string | TestConsumer): Promise<RunnableTestConsumer> {
14
+ return new RunnableTestConsumer(await TestConsumerRegistry.getInstance(consumer));
15
15
  }
16
16
 
17
17
  #consumers: TestConsumer[];
@@ -27,11 +27,9 @@ export class RunnableTestConsumer implements TestConsumer {
27
27
  }
28
28
  }
29
29
 
30
- onStart(): void {
30
+ async onStart(files: string[]): Promise<void> {
31
31
  for (const c of this.#consumers) {
32
- if (c.onStart) {
33
- c.onStart();
34
- }
32
+ await c.onStart?.(files);
35
33
  }
36
34
  }
37
35
 
@@ -44,19 +42,17 @@ export class RunnableTestConsumer implements TestConsumer {
44
42
  }
45
43
  }
46
44
 
47
- summarize(): TestResultsSummarizer | undefined {
45
+ async summarize(): Promise<TestResultsSummarizer | undefined> {
48
46
  if (this.#results) {
49
47
  for (const c of this.#consumers) {
50
- if (c.onSummary) {
51
- c.onSummary(this.#results.summary);
52
- }
48
+ await c.onSummary?.(this.#results.summary);
53
49
  }
54
50
  return this.#results;
55
51
  }
56
52
  }
57
53
 
58
- summarizeAsBoolean(): boolean {
59
- const result = this.summarize();
54
+ async summarizeAsBoolean(): Promise<boolean> {
55
+ const result = await this.summarize();
60
56
  if (result) {
61
57
  return result.summary.failed <= 0;
62
58
  } else {
@@ -0,0 +1,92 @@
1
+ import { GlobalTerminal, TermStyleInput, Terminal } from '@travetto/terminal';
2
+ import { ManualAsyncIterator } from '@travetto/worker';
3
+ import { RootIndex } from '@travetto/manifest';
4
+
5
+ import { SuitesSummary, TestConsumer } from '../types';
6
+ import { Consumable } from '../registry';
7
+
8
+ import { TestEvent } from '../../model/event';
9
+ import { TestResult } from '../../model/test';
10
+ import { SuiteRegistry } from '../../registry/suite';
11
+ import { TapEmitter } from './tap';
12
+
13
+ /**
14
+ * Streamed summary results
15
+ */
16
+ @Consumable('tap-streamed')
17
+ export class TapStreamedEmitter implements TestConsumer {
18
+
19
+ static makeProgressBar(term: Terminal, total: number): (t: TestResult, idx: number) => string {
20
+ let failed = 0;
21
+ const palette: TermStyleInput[] = [
22
+ { text: 'white', background: 'darkGreen' },
23
+ { text: 'white', background: 'darkRed' }
24
+ ];
25
+ const styles = palette.map(s => GlobalTerminal.colorer(s));
26
+
27
+ return (t: TestResult, idx: number): string => {
28
+ if (t.status === 'failed') {
29
+ failed += 1;
30
+ }
31
+ const i = idx + 1;
32
+ const digits = total.toString().length;
33
+ const paddedI = `${i}`.padStart(digits);
34
+ const paddedFailed = `${failed}`.padStart(digits);
35
+ const line = `Tests ${paddedI}/${total} [${paddedFailed} failed] -- ${t.classId}`.padEnd(term.width);
36
+ const pos = Math.trunc(line.length * (i / total));
37
+ const colorer = styles[Math.min(failed, styles.length - 1)];
38
+ return `${colorer(line.substring(0, pos))}${line.substring(pos)}`;
39
+ };
40
+ }
41
+
42
+ #terminal: Terminal;
43
+ #results = new ManualAsyncIterator<TestResult>();
44
+ #progress: Promise<unknown> | undefined;
45
+ #consumer: TapEmitter;
46
+
47
+ constructor(terminal: Terminal = new Terminal({ output: process.stderr })) {
48
+ this.#terminal = terminal;
49
+ this.#consumer = new TapEmitter(this.#terminal);
50
+ }
51
+
52
+ async onStart(files: string[]): Promise<void> {
53
+ this.#consumer.onStart();
54
+
55
+ // Load all tests
56
+ for (const file of files) {
57
+ await import(RootIndex.getFromSource(file)!.import);
58
+ }
59
+
60
+ await SuiteRegistry.init();
61
+
62
+ const suites = SuiteRegistry.getClasses();
63
+ const total = suites
64
+ .map(c => SuiteRegistry.get(c))
65
+ .filter(c => !RootIndex.getFunctionMetadata(c.class)?.abstract)
66
+ .reduce((acc, c) => acc + (c.tests?.length ?? 0), 0);
67
+
68
+ this.#progress = this.#terminal.streamToPosition(this.#results,
69
+ TapStreamedEmitter.makeProgressBar(this.#terminal, total),
70
+ { position: 'bottom' }
71
+ );
72
+ }
73
+
74
+ onEvent(ev: TestEvent): void {
75
+ if (ev.type === 'test' && ev.phase === 'after') {
76
+ const { test } = ev;
77
+ this.#results.add(test);
78
+ if (test.status === 'failed') {
79
+ this.#consumer.onEvent(ev);
80
+ }
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Summarize all results
86
+ */
87
+ async onSummary(summary: SuitesSummary): Promise<void> {
88
+ this.#results.close();
89
+ await this.#progress;
90
+ await this.#consumer.onSummary?.(summary);
91
+ }
92
+ }