@travetto/test 5.0.0-rc.8 → 5.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.
package/README.md CHANGED
@@ -221,12 +221,13 @@ To run the tests you can either call the [Command Line Interface](https://github
221
221
  ```bash
222
222
  $ trv test --help
223
223
 
224
- Usage: test [options] [first:string] [regexes...:string]
224
+ Usage: test [options] [first:string] [globs...:string]
225
225
 
226
226
  Options:
227
227
  -f, --format <string> Output format for test results (default: "tap")
228
228
  -c, --concurrency <number> Number of tests to run concurrently (default: 4)
229
229
  -m, --mode <single|standard> Test run mode (default: "standard")
230
+ -t, --tags <string> Tags to target or exclude
230
231
  -h, --help display help for command
231
232
  ```
232
233
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/test",
3
- "version": "5.0.0-rc.8",
3
+ "version": "5.0.0",
4
4
  "description": "Declarative test framework",
5
5
  "keywords": [
6
6
  "unit-testing",
@@ -27,15 +27,15 @@
27
27
  "directory": "module/test"
28
28
  },
29
29
  "dependencies": {
30
- "@travetto/runtime": "^5.0.0-rc.8",
31
- "@travetto/registry": "^5.0.0-rc.8",
32
- "@travetto/terminal": "^5.0.0-rc.8",
33
- "@travetto/worker": "^5.0.0-rc.8",
34
- "yaml": "^2.4.5"
30
+ "@travetto/registry": "^5.0.0",
31
+ "@travetto/runtime": "^5.0.0",
32
+ "@travetto/terminal": "^5.0.0",
33
+ "@travetto/worker": "^5.0.0",
34
+ "yaml": "^2.5.0"
35
35
  },
36
36
  "peerDependencies": {
37
- "@travetto/cli": "^5.0.0-rc.8",
38
- "@travetto/transformer": "^5.0.0-rc.5"
37
+ "@travetto/cli": "^5.0.0",
38
+ "@travetto/transformer": "^5.0.0"
39
39
  },
40
40
  "peerDependenciesMeta": {
41
41
  "@travetto/transformer": {
@@ -26,15 +26,15 @@ class $AssertCapture {
26
26
 
27
27
  // Emit and collect, every assertion as it occurs
28
28
  const handler = (a: CaptureAssert): void => {
29
- const assrt: Assertion = {
29
+ const asrt: Assertion = {
30
30
  ...a,
31
31
  import: a.import ?? a.module!.join('/'),
32
32
  classId: test.classId,
33
33
  methodName: test.methodName
34
34
  };
35
- assertions.push(assrt);
35
+ assertions.push(asrt);
36
36
  if (listener) {
37
- listener(assrt);
37
+ listener(asrt);
38
38
  }
39
39
  };
40
40
 
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert';
2
2
 
3
- import { AppError, ClassInstance, Class } from '@travetto/runtime';
3
+ import { AppError, Class, castTo, castKey, asConstructable } from '@travetto/runtime';
4
4
 
5
5
  import { ThrowableError, TestConfig, Assertion } from '../model/test';
6
6
  import { AssertCapture, CaptureAssert } from './capture';
@@ -34,36 +34,35 @@ export class AssertCheck {
34
34
  // Invert check for negative
35
35
  const assertFn = positive ? assert : (x: unknown, msg?: string): unknown => assert(!x, msg);
36
36
 
37
- /* eslint-disable @typescript-eslint/consistent-type-assertions */
38
37
  // Check fn to call
39
38
  if (fn === 'fail') {
40
39
  if (args.length > 1) {
41
- [assertion.actual, assertion.expected, assertion.message, assertion.operator] = args as [unknown, unknown, string, string];
40
+ [assertion.actual, assertion.expected, assertion.message, assertion.operator] = castTo(args);
42
41
  } else {
43
- [assertion.message] = args as [string];
42
+ [assertion.message] = castTo(args);
44
43
  }
45
44
  } else if (/throw|reject/i.test(fn)) {
46
45
  assertion.operator = fn;
47
46
  if (typeof args[1] !== 'string') {
48
- [, assertion.expected, assertion.message] = args as [unknown, unknown, string];
47
+ [, assertion.expected, assertion.message] = castTo(args);
49
48
  } else {
50
- [, assertion.message] = args as [unknown, string];
49
+ [, assertion.message] = castTo(args);
51
50
  }
52
51
  } else if (fn === 'ok' || fn === 'assert') {
53
52
  fn = assertion.operator = 'ok';
54
- [assertion.actual, assertion.message] = args as [unknown, string];
53
+ [assertion.actual, assertion.message] = castTo(args);
55
54
  assertion.expected = { toClean: (): string => positive ? 'truthy' : 'falsy' };
56
55
  common.state = 'should be';
57
56
  } else if (fn === 'includes') {
58
57
  assertion.operator = fn;
59
- [assertion.actual, assertion.expected, assertion.message] = args as [unknown, unknown, string];
58
+ [assertion.actual, assertion.expected, assertion.message] = castTo(args);
60
59
  } else if (fn === 'instanceof') {
61
60
  assertion.operator = fn;
62
- [assertion.actual, assertion.expected, assertion.message] = args as [unknown, unknown, string];
63
- assertion.actual = (assertion.actual as ClassInstance)?.constructor;
61
+ [assertion.actual, assertion.expected, assertion.message] = castTo(args);
62
+ assertion.actual = asConstructable(assertion.actual)?.constructor;
64
63
  } else { // Handle unknown
65
64
  assertion.operator = fn ?? '';
66
- [assertion.actual, assertion.expected, assertion.message] = args as [unknown, unknown, string];
65
+ [assertion.actual, assertion.expected, assertion.message] = castTo(args);
67
66
  }
68
67
 
69
68
  try {
@@ -76,25 +75,25 @@ export class AssertCheck {
76
75
  assertion.expected = AssertUtil.cleanValue(assertion.expected);
77
76
  }
78
77
 
79
- const [actual, expected, message] = args as [unknown, unknown, string];
78
+ const [actual, expected, message]: [unknown, unknown, string] = castTo(args);
80
79
 
81
80
  // Actually run the assertion
82
81
  switch (fn) {
83
- case 'includes': assertFn((actual as unknown[]).includes(expected), message); break;
84
- case 'test': assertFn((expected as RegExp).test(actual as string), message); break;
85
- case 'instanceof': assertFn(actual instanceof (expected as Class), message); break;
86
- case 'in': assertFn((actual as string) in (expected as object), message); break;
87
- case 'lessThan': assertFn((actual as number) < (expected as number), message); break;
88
- case 'lessThanEqual': assertFn((actual as number) <= (expected as number), message); break;
89
- case 'greaterThan': assertFn((actual as number) > (expected as number), message); break;
90
- case 'greaterThanEqual': assertFn((actual as number) >= (expected as number), message); break;
91
- case 'ok': assertFn(...args as [unknown, string]); break;
82
+ case 'includes': assertFn(castTo<unknown[]>(actual).includes(expected), message); break;
83
+ case 'test': assertFn(castTo<RegExp>(expected).test(castTo(actual)), message); break;
84
+ case 'instanceof': assertFn(actual instanceof castTo<Class>(expected), message); break;
85
+ case 'in': assertFn(castTo<string>(actual) in castTo<object>(expected), message); break;
86
+ case 'lessThan': assertFn(castTo<number>(actual) < castTo<number>(expected), message); break;
87
+ case 'lessThanEqual': assertFn(castTo<number>(actual) <= castTo<number>(expected), message); break;
88
+ case 'greaterThan': assertFn(castTo<number>(actual) > castTo<number>(expected), message); break;
89
+ case 'greaterThanEqual': assertFn(castTo<number>(actual) >= castTo<number>(expected), message); break;
90
+ case 'ok': assertFn(...castTo<Parameters<typeof assertFn>>(args)); break;
92
91
  default:
93
- if (fn && assert[fn as keyof typeof assert]) { // Assert call
92
+ if (fn && assert[castKey<typeof assert>(fn)]) { // Assert call
94
93
  if (/not/i.test(fn)) {
95
94
  common.state = 'should not';
96
95
  }
97
- assert[fn as 'ok'].apply(null, args as [boolean, string | undefined]);
96
+ assert[castTo<'ok'>(fn)].apply(null, castTo(args));
98
97
  }
99
98
  }
100
99
 
@@ -115,7 +114,6 @@ export class AssertCheck {
115
114
  }
116
115
  throw err;
117
116
  }
118
- /* eslint-enable @typescript-eslint/consistent-type-assertions */
119
117
  }
120
118
 
121
119
  /**
@@ -267,7 +265,7 @@ export class AssertCheck {
267
265
  * Look for any unhandled exceptions
268
266
  */
269
267
  static checkUnhandled(test: TestConfig, err: Error | assert.AssertionError): void {
270
- let line = AssertUtil.getPositionOfError(err, test.import).line;
268
+ let line = AssertUtil.getPositionOfError(err, test.sourceImport ?? test.import).line;
271
269
  if (line === 1) {
272
270
  line = test.lineStart;
273
271
  }
@@ -1,13 +1,13 @@
1
1
  import util from 'node:util';
2
+ import path from 'node:path';
2
3
 
3
- import { Runtime, RuntimeIndex } from '@travetto/runtime';
4
+ import { asFull, Class, Runtime, RuntimeIndex } from '@travetto/runtime';
4
5
 
5
6
  import { TestConfig, Assertion, TestResult } from '../model/test';
6
- import { SuiteConfig } from '../model/suite';
7
+ import { SuiteConfig, SuiteFailure, SuiteResult } from '../model/suite';
7
8
 
8
9
  function isCleanable(o: unknown): o is { toClean(): unknown } {
9
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
10
- return !!o && !!(o as { toClean: unknown }).toClean;
10
+ return !!o && typeof o === 'object' && 'toClean' in o && typeof o.toClean === 'function';
11
11
  }
12
12
 
13
13
  /**
@@ -19,6 +19,7 @@ export class AssertUtil {
19
19
  */
20
20
  static cleanValue(val: unknown): unknown {
21
21
  switch (typeof val) {
22
+ case 'number': case 'boolean': case 'bigint': case 'string': case 'undefined': return val;
22
23
  case 'object': {
23
24
  if (isCleanable(val)) {
24
25
  return val.toClean();
@@ -27,7 +28,6 @@ export class AssertUtil {
27
28
  }
28
29
  break;
29
30
  }
30
- case 'undefined': case 'string': case 'number': case 'bigint': case 'boolean': return JSON.stringify(val);
31
31
  case 'function': {
32
32
  if (val.Ⲑid || !val.constructor) {
33
33
  return val.name;
@@ -35,7 +35,6 @@ export class AssertUtil {
35
35
  break;
36
36
  }
37
37
  }
38
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
39
38
  return util.inspect(val, false, 1).replace(/\n/g, ' ');
40
39
  }
41
40
 
@@ -89,7 +88,7 @@ export class AssertUtil {
89
88
  /**
90
89
  * Generate a suite error given a suite config, and an error
91
90
  */
92
- static generateSuiteError(suite: SuiteConfig, methodName: string, error: Error): { assert: Assertion, testResult: TestResult, testConfig: TestConfig } {
91
+ static generateSuiteFailure(suite: SuiteConfig, methodName: string, error: Error): SuiteFailure {
93
92
  const { import: imp, ...pos } = this.getPositionOfError(error, suite.import);
94
93
  let line = pos.line;
95
94
 
@@ -110,11 +109,24 @@ export class AssertUtil {
110
109
  ...coreAll,
111
110
  status: 'failed', error, duration: 0, durationTotal: 0, assertions: [assert], output: {}
112
111
  };
113
- const testConfig: TestConfig = {
112
+ const test: TestConfig = {
114
113
  ...coreAll,
115
114
  class: suite.class, skip: false
116
115
  };
117
116
 
118
- return { assert, testResult, testConfig };
117
+ return { assert, testResult, test, suite };
118
+ }
119
+
120
+ /**
121
+ * Define import failure as a SuiteFailure object
122
+ */
123
+ static gernerateImportFailure(imp: string, err: Error): SuiteFailure {
124
+ const name = path.basename(imp);
125
+ const classId = `${RuntimeIndex.getFromImport(imp)?.id}○${name}`;
126
+ const suite = asFull<SuiteConfig & SuiteResult>({
127
+ class: asFull<Class>({ name }), classId, duration: 0, lineStart: 1, lineEnd: 1, import: imp
128
+ });
129
+ err.message = err.message.replaceAll(Runtime.mainSourcePath, '.');
130
+ return this.generateSuiteFailure(suite, 'require', err);
119
131
  }
120
132
  }
@@ -1,4 +1,4 @@
1
- import type { Class, ConcreteClass } from '@travetto/runtime';
1
+ import { classConstruct, type Class } from '@travetto/runtime';
2
2
  import { TestConsumer } from './types';
3
3
 
4
4
  /**
@@ -45,8 +45,7 @@ class $TestConsumerRegistry {
45
45
  await this.manualInit();
46
46
 
47
47
  return typeof consumer === 'string' ?
48
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
49
- new ((this.get(consumer) ?? this.#primary) as ConcreteClass)() :
48
+ classConstruct(this.get(consumer) ?? this.#primary) :
50
49
  consumer;
51
50
  }
52
51
  }
@@ -5,18 +5,18 @@ import { TestEvent, } from '../model/event';
5
5
 
6
6
  export type SerializedError = { $?: boolean, message: string, stack?: string, name: string };
7
7
 
8
- function isSerialized(e: unknown): e is SerializedError {
8
+ function isError(e: unknown): e is SerializedError {
9
9
  return !!e && (typeof e === 'object') && '$' in e;
10
10
  }
11
11
 
12
- export class ErrorUtil {
12
+ export class SerializeUtil {
13
13
 
14
14
  /**
15
15
  * Prepare error for transmission
16
16
  */
17
- static serializeError(e: Error | SerializedError): SerializedError;
17
+ static serializeError(e: Error | SerializedError): Error;
18
18
  static serializeError(e: undefined): undefined;
19
- static serializeError(e: Error | SerializedError | undefined): SerializedError | undefined {
19
+ static serializeError(e: Error | SerializedError | undefined): Error | undefined {
20
20
  let error: SerializedError | undefined;
21
21
 
22
22
  if (e) {
@@ -41,7 +41,7 @@ export class ErrorUtil {
41
41
  static deserializeError(e: Error | SerializedError): Error;
42
42
  static deserializeError(e: undefined): undefined;
43
43
  static deserializeError(e: Error | SerializedError | undefined): Error | undefined {
44
- if (isSerialized(e)) {
44
+ if (isError(e)) {
45
45
  const err = new Error();
46
46
 
47
47
  for (const k of TypedObject.keys(e)) {
@@ -54,27 +54,18 @@ export class ErrorUtil {
54
54
  err.stack = e.stack;
55
55
  err.name = e.name;
56
56
  return err;
57
- } else if (e) {
57
+ } else {
58
58
  return e;
59
59
  }
60
60
  }
61
61
 
62
62
  /**
63
- * Serialize all errors for a given test for transmission between parent/child
63
+ * Serialize to JSON
64
64
  */
65
- static serializeTestErrors(out: TestEvent): void {
66
- if (out.phase === 'after') {
67
- if (out.type === 'test') {
68
- if (out.test.error) {
69
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
70
- out.test.error = this.serializeError(out.test.error) as Error;
71
- }
72
- } else if (out.type === 'assertion') {
73
- if (out.assertion.error) {
74
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
75
- out.assertion.error = this.serializeError(out.assertion.error) as Error;
76
- }
77
- }
78
- }
65
+ static serializeToJSON(out: TestEvent): string {
66
+ return JSON.stringify(out, (_, v) =>
67
+ v instanceof Error ? this.serializeError(v) :
68
+ typeof v === 'bigint' ? v.toString() : v
69
+ );
79
70
  }
80
71
  }
@@ -7,19 +7,19 @@ import { TestEvent } from '../../model/event';
7
7
  import { TestResult } from '../../model/test';
8
8
  import { SuiteResult } from '../../model/suite';
9
9
  import { SuiteRegistry } from '../../registry/suite';
10
+ import { DelegatingConsumer } from './delegating';
10
11
 
11
12
  /**
12
13
  * Cumulative Summary consumer
13
14
  */
14
- export class CumulativeSummaryConsumer implements TestConsumer {
15
+ export class CumulativeSummaryConsumer extends DelegatingConsumer {
15
16
  /**
16
17
  * Total state of all tests run so far
17
18
  */
18
19
  #state: Record<string, Record<string, TestResult['status']>> = {};
19
- #target: TestConsumer;
20
20
 
21
21
  constructor(target: TestConsumer) {
22
- this.#target = target;
22
+ super([target]);
23
23
  }
24
24
 
25
25
  /**
@@ -29,16 +29,9 @@ export class CumulativeSummaryConsumer implements TestConsumer {
29
29
  summarizeSuite(test: TestResult): SuiteResult {
30
30
  // Was only loading to verify existence (TODO: double-check)
31
31
  if (existsSync(RuntimeIndex.getFromImport(test.import)!.sourceFile)) {
32
- this.#state[test.classId] = this.#state[test.classId] ?? {};
33
- this.#state[test.classId][test.methodName] = test.status;
34
- const SuiteCls = SuiteRegistry.getClasses().find(x =>
35
- x.Ⲑid === test.classId
36
- )!;
37
- if (SuiteCls) {
38
- return this.computeTotal(SuiteCls);
39
- } else {
40
- return this.removeClass(test.classId);
41
- }
32
+ (this.#state[test.classId] ??= {})[test.methodName] = test.status;
33
+ const SuiteCls = SuiteRegistry.getClasses().find(x => x.Ⲑid === test.classId);
34
+ return SuiteCls ? this.computeTotal(SuiteCls) : this.removeClass(test.classId);
42
35
  } else {
43
36
  return this.removeClass(test.classId);
44
37
  }
@@ -83,14 +76,13 @@ export class CumulativeSummaryConsumer implements TestConsumer {
83
76
  * Listen for event, process the full event, and if the event is an after test,
84
77
  * send a full suite summary
85
78
  */
86
- onEvent(e: TestEvent): void {
87
- this.#target.onEvent(e);
79
+ onEventDone(e: TestEvent): void {
88
80
  try {
89
81
  if (e.type === 'test' && e.phase === 'after') {
90
- this.#target.onEvent({
82
+ this.onEvent({
91
83
  type: 'suite',
92
84
  phase: 'after',
93
- suite: this.summarizeSuite(e.test)
85
+ suite: this.summarizeSuite(e.test),
94
86
  });
95
87
  }
96
88
  } catch (err) {
@@ -0,0 +1,58 @@
1
+ import { SuitesSummary, TestConsumer, TestRunState } from '../types';
2
+ import { TestEvent } from '../../model/event';
3
+
4
+ /**
5
+ * Delegating event consumer
6
+ */
7
+ export abstract class DelegatingConsumer implements TestConsumer {
8
+ #consumers: TestConsumer[];
9
+ #transformer?: (ev: TestEvent) => typeof ev;
10
+ #filter?: (ev: TestEvent) => boolean;
11
+
12
+ constructor(consumers: TestConsumer[]) {
13
+ this.#consumers = consumers;
14
+ for (const c of consumers) {
15
+ c.onEvent = c.onEvent.bind(c);
16
+ }
17
+ }
18
+
19
+ withTransformer(transformer: (ev: TestEvent) => typeof ev): this {
20
+ this.#transformer = transformer;
21
+ return this;
22
+ }
23
+
24
+ withFilter(filter: (ev: TestEvent) => boolean): this {
25
+ this.#filter = filter;
26
+ return this;
27
+ }
28
+
29
+ async onStart(state: TestRunState): Promise<void> {
30
+ for (const c of this.#consumers) {
31
+ await c.onStart?.(state);
32
+ }
33
+ }
34
+
35
+ onEvent(e: TestEvent): void {
36
+ if (this.#transformer) {
37
+ e = this.#transformer(e);
38
+ }
39
+ if (this.#filter?.(e) === false) {
40
+ return;
41
+ }
42
+ for (const c of this.#consumers) {
43
+ c.onEvent(e);
44
+ }
45
+
46
+ this.onEventDone?.(e);
47
+ }
48
+
49
+ async summarize(summary?: SuitesSummary): Promise<void> {
50
+ if (summary) {
51
+ for (const c of this.#consumers) {
52
+ await c.onSummary?.(summary);
53
+ }
54
+ }
55
+ }
56
+
57
+ onEventDone?(e: TestEvent): void;
58
+ }
@@ -2,7 +2,7 @@ import { Writable } from 'node:stream';
2
2
 
3
3
  import { TestEvent } from '../../model/event';
4
4
  import { TestConsumer } from '../types';
5
- import { ErrorUtil } from '../error';
5
+ import { SerializeUtil } from '../serialize';
6
6
  import { Consumable } from '../registry';
7
7
 
8
8
  /**
@@ -17,8 +17,6 @@ export class EventStreamer implements TestConsumer {
17
17
  }
18
18
 
19
19
  onEvent(event: TestEvent): void {
20
- const out = { ...event };
21
- ErrorUtil.serializeTestErrors(out);
22
- this.#stream.write(`${JSON.stringify(out)}\n`);
20
+ this.#stream.write(`${SerializeUtil.serializeToJSON(event)}\n`);
23
21
  }
24
22
  }
@@ -2,7 +2,7 @@ import { ChildCommChannel } from '@travetto/worker';
2
2
 
3
3
  import { TestEvent } from '../../model/event';
4
4
  import { TestConsumer } from '../types';
5
- import { ErrorUtil } from '../error';
5
+ import { SerializeUtil } from '../serialize';
6
6
  import { Consumable } from '../registry';
7
7
 
8
8
  /**
@@ -12,8 +12,6 @@ import { Consumable } from '../registry';
12
12
  export class ExecutionEmitter extends ChildCommChannel<TestEvent> implements TestConsumer {
13
13
 
14
14
  onEvent(event: TestEvent): void {
15
- const out = { ...event };
16
- ErrorUtil.serializeTestErrors(out);
17
- this.send(event.type, out);
15
+ this.send(event.type, JSON.parse(SerializeUtil.serializeToJSON(event)));
18
16
  }
19
17
  }
@@ -1,62 +1,33 @@
1
- import { TestConsumer, TestRunState } from '../types';
1
+ import { TestConsumer } from '../types';
2
2
  import { TestResultsSummarizer } from './summarizer';
3
3
  import { TestConsumerRegistry } from '../registry';
4
4
  import { TestEvent } from '../../model/event';
5
+ import { DelegatingConsumer } from './delegating';
5
6
 
6
7
  /**
7
8
  * Test consumer with support for multiple nested consumers, and summarization
8
9
  */
9
- export class RunnableTestConsumer implements TestConsumer {
10
+ export class RunnableTestConsumer extends DelegatingConsumer {
10
11
  /**
11
12
  * Build a runnable test consumer given a format or a full consumer
12
13
  */
13
14
  static async get(consumer: string | TestConsumer): Promise<RunnableTestConsumer> {
14
- return new RunnableTestConsumer(await TestConsumerRegistry.getInstance(consumer));
15
+ return new RunnableTestConsumer([await TestConsumerRegistry.getInstance(consumer)]);
15
16
  }
16
17
 
17
- #consumers: TestConsumer[];
18
- #results: TestResultsSummarizer | undefined;
18
+ #results?: TestResultsSummarizer;
19
19
 
20
- constructor(...consumers: TestConsumer[]) {
21
- this.#consumers = consumers;
22
- for (const c of consumers) {
23
- if (!this.#results && c.onSummary) { // If expecting summary
24
- this.#results = new TestResultsSummarizer();
25
- }
26
- c.onEvent = c.onEvent.bind(c);
27
- }
20
+ constructor(consumers: TestConsumer[]) {
21
+ super(consumers);
22
+ this.#results = consumers.find(x => !!x.onSummary) ? new TestResultsSummarizer() : undefined;
28
23
  }
29
24
 
30
- async onStart(state: TestRunState): Promise<void> {
31
- for (const c of this.#consumers) {
32
- await c.onStart?.(state);
33
- }
34
- }
35
-
36
- onEvent(e: TestEvent): void {
37
- if (this.#results) {
38
- this.#results.onEvent(e);
39
- }
40
- for (const c of this.#consumers) {
41
- c.onEvent(e);
42
- }
43
- }
44
-
45
- async summarize(): Promise<TestResultsSummarizer | undefined> {
46
- if (this.#results) {
47
- for (const c of this.#consumers) {
48
- await c.onSummary?.(this.#results.summary);
49
- }
50
- return this.#results;
51
- }
25
+ onEventDone(e: TestEvent): void {
26
+ this.#results?.onEvent(e);
52
27
  }
53
28
 
54
29
  async summarizeAsBoolean(): Promise<boolean> {
55
- const result = await this.summarize();
56
- if (result) {
57
- return result.summary.failed <= 0;
58
- } else {
59
- return true;
60
- }
30
+ await this.summarize(this.#results?.summary);
31
+ return (this.#results?.summary.failed ?? 0) <= 0;
61
32
  }
62
33
  }
@@ -1,6 +1,5 @@
1
- import { Util } from '@travetto/runtime';
1
+ import { Util, AsyncQueue } from '@travetto/runtime';
2
2
  import { StyleUtil, Terminal, TerminalUtil } from '@travetto/terminal';
3
- import { WorkQueue } from '@travetto/worker';
4
3
 
5
4
  import { TestEvent } from '../../model/event';
6
5
  import { TestResult } from '../../model/test';
@@ -17,7 +16,7 @@ import { TapEmitter } from './tap';
17
16
  export class TapStreamedEmitter implements TestConsumer {
18
17
 
19
18
  #terminal: Terminal;
20
- #results = new WorkQueue<TestResult>();
19
+ #results = new AsyncQueue<TestResult>();
21
20
  #progress: Promise<unknown> | undefined;
22
21
  #consumer: TapEmitter;
23
22
 
@@ -30,16 +29,20 @@ export class TapStreamedEmitter implements TestConsumer {
30
29
  this.#consumer.onStart();
31
30
 
32
31
  let failed = 0;
33
- const succ = StyleUtil.getStyle({ text: '#e5e5e5', background: '#026020' }); // White on dark green
32
+ let skipped = 0;
33
+ let completed = 0;
34
+ const success = StyleUtil.getStyle({ text: '#e5e5e5', background: '#026020' }); // White on dark green
34
35
  const fail = StyleUtil.getStyle({ text: '#e5e5e5', background: '#8b0000' }); // White on dark red
35
36
  this.#progress = this.#terminal.streamToBottom(
36
37
  Util.mapAsyncItr(
37
38
  this.#results,
38
39
  (value, idx) => {
39
40
  failed += (value.status === 'failed' ? 1 : 0);
40
- return { value: `Tests %idx/%total [${failed} failed] -- ${value.classId}`, total: state.testCount, idx };
41
+ skipped += (value.status === 'skipped' ? 1 : 0);
42
+ completed += (value.status !== 'skipped' ? 1 : 0);
43
+ return { value: `Tests %idx/%total [${failed} failed, ${skipped} skipped] -- ${value.classId}`, total: state.testCount, idx: completed };
41
44
  },
42
- TerminalUtil.progressBarUpdater(this.#terminal, { style: () => ({ complete: failed ? fail : succ }) })
45
+ TerminalUtil.progressBarUpdater(this.#terminal, { style: () => ({ complete: failed ? fail : success }) })
43
46
  ),
44
47
  { minDelay: 100 }
45
48
  );
@@ -5,7 +5,7 @@ import { stringify } from 'yaml';
5
5
  import { TestEvent } from '../../model/event';
6
6
  import { SuitesSummary, TestConsumer } from '../types';
7
7
  import { Consumable } from '../registry';
8
- import { ErrorUtil } from '../error';
8
+ import { SerializeUtil } from '../serialize';
9
9
  import { TestResultsEnhancer, CONSOLE_ENHANCER } from '../enhancer';
10
10
 
11
11
  /**
@@ -102,7 +102,7 @@ export class TapEmitter implements TestConsumer {
102
102
  // Handle error
103
103
  if (test.status === 'failed') {
104
104
  if (test.error && test.error.name !== 'AssertionError') {
105
- const err = ErrorUtil.deserializeError(test.error);
105
+ const err = SerializeUtil.deserializeError(test.error);
106
106
  this.logMeta({ error: err instanceof AppError ? err.toJSON() : err });
107
107
  }
108
108
  }
@@ -1,4 +1,4 @@
1
- import { Class, ClassInstance, describeFunction } from '@travetto/runtime';
1
+ import { castTo, Class, ClassInstance, describeFunction } from '@travetto/runtime';
2
2
 
3
3
  import { SuiteRegistry } from '../registry/suite';
4
4
  import { SuiteConfig } from '../model/suite';
@@ -23,17 +23,16 @@ export function Suite(description?: string | Partial<SuiteConfig>, ...rest: Part
23
23
  Object.assign(extra, r);
24
24
  }
25
25
 
26
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
27
- const decorator = ((target: Class) => {
26
+ const dec = (target: Class): typeof target => {
28
27
  const cfg = { description: descriptionString, ...extra };
29
28
  if (describeFunction(target).abstract) {
30
29
  cfg.skip = true;
31
30
  }
32
31
  SuiteRegistry.register(target, cfg);
33
32
  return target;
34
- }) as ClassDecorator;
33
+ };
35
34
 
36
- return decorator;
35
+ return castTo(dec);
37
36
  }
38
37
 
39
38
  function listener(phase: SuitePhase) {