vest 6.0.0 → 6.0.2

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 (66) hide show
  1. package/dist/exports/SuiteSerializer.cjs +45 -1
  2. package/dist/exports/SuiteSerializer.cjs.map +1 -1
  3. package/dist/exports/SuiteSerializer.mjs +45 -1
  4. package/dist/exports/SuiteSerializer.mjs.map +1 -1
  5. package/dist/exports/classnames.cjs +2 -2
  6. package/dist/exports/classnames.mjs +2 -2
  7. package/dist/exports/debounce.cjs +1 -1
  8. package/dist/exports/debounce.mjs +1 -1
  9. package/dist/exports/memo.cjs +1 -1
  10. package/dist/exports/memo.mjs +1 -1
  11. package/dist/exports/parser.cjs +2 -2
  12. package/dist/exports/parser.mjs +2 -2
  13. package/dist/{parser-CLhCAAYG.cjs → parser-DurNd9af.cjs} +2 -2
  14. package/dist/{parser-CLhCAAYG.cjs.map → parser-DurNd9af.cjs.map} +1 -1
  15. package/dist/{parser-Ch9qn6Hm.mjs → parser-TvzEAVLQ.mjs} +2 -2
  16. package/dist/{parser-Ch9qn6Hm.mjs.map → parser-TvzEAVLQ.mjs.map} +1 -1
  17. package/dist/{vest-CKYaivPf.cjs → vest-Lg6ZJuyu.cjs} +46 -12
  18. package/dist/vest-Lg6ZJuyu.cjs.map +1 -0
  19. package/dist/{vest-BEj6iXsK.mjs → vest-WSPRosdH.mjs} +47 -13
  20. package/dist/vest-WSPRosdH.mjs.map +1 -0
  21. package/dist/vest.cjs +1 -1
  22. package/dist/vest.mjs +1 -1
  23. package/package.json +6 -6
  24. package/src/core/Runtime.ts +3 -2
  25. package/src/exports/SuiteSerializer.ts +104 -1
  26. package/src/exports/__tests__/SuiteSerializer.test.ts +58 -1
  27. package/src/exports/__tests__/classnames.test.ts +24 -1
  28. package/src/exports/__tests__/parser.test.ts +23 -1
  29. package/src/suite/SuiteTypes.ts +15 -5
  30. package/src/suite/__tests__/runSummary.integration.test.ts +72 -0
  31. package/src/suite/__tests__/schema.types.test.ts +12 -0
  32. package/src/suite/useCreateSuiteRunner.ts +62 -41
  33. package/src/suiteResult/SuiteResultTypes.ts +22 -4
  34. package/src/suiteResult/selectors/useProduceSuiteSummary.ts +3 -2
  35. package/src/suiteResult/suiteResult.ts +37 -10
  36. package/types/{IsolateSuite-DnMtFHBW.d.cts → IsolateSuite-3gZF4eDG.d.mts} +11 -6
  37. package/types/IsolateSuite-3gZF4eDG.d.mts.map +1 -0
  38. package/types/{IsolateSuite-JdFYn2DA.d.mts → IsolateSuite-WsmXGGR-.d.cts} +11 -6
  39. package/types/IsolateSuite-WsmXGGR-.d.cts.map +1 -0
  40. package/types/{SuiteTypes-DESBCvZ7.d.cts → SuiteTypes-BNCL13HG.d.cts} +8 -7
  41. package/types/SuiteTypes-BNCL13HG.d.cts.map +1 -0
  42. package/types/{SuiteTypes-cjI5BA_k.d.mts → SuiteTypes-hg7F7swJ.d.mts} +8 -7
  43. package/types/SuiteTypes-hg7F7swJ.d.mts.map +1 -0
  44. package/types/exports/SuiteSerializer.d.cts +2 -2
  45. package/types/exports/SuiteSerializer.d.mts +2 -2
  46. package/types/exports/classnames.d.cts +2 -2
  47. package/types/exports/classnames.d.mts +2 -2
  48. package/types/exports/debounce.d.cts +1 -1
  49. package/types/exports/debounce.d.mts +1 -1
  50. package/types/exports/memo.d.cts +1 -1
  51. package/types/exports/memo.d.mts +1 -1
  52. package/types/exports/parser.d.cts +2 -2
  53. package/types/exports/parser.d.mts +2 -2
  54. package/types/{parser-BsrjuZ4_.d.cts → parser-DqW8l1IB.d.cts} +2 -2
  55. package/types/{parser-BsrjuZ4_.d.cts.map → parser-DqW8l1IB.d.cts.map} +1 -1
  56. package/types/{parser-CqvCfNJR.d.mts → parser-uCTQIcV6.d.mts} +2 -2
  57. package/types/{parser-CqvCfNJR.d.mts.map → parser-uCTQIcV6.d.mts.map} +1 -1
  58. package/types/vest.d.cts +2 -2
  59. package/types/vest.d.mts +2 -2
  60. package/types/vest.d.ts +2 -2
  61. package/dist/vest-BEj6iXsK.mjs.map +0 -1
  62. package/dist/vest-CKYaivPf.cjs.map +0 -1
  63. package/types/IsolateSuite-DnMtFHBW.d.cts.map +0 -1
  64. package/types/IsolateSuite-JdFYn2DA.d.mts.map +0 -1
  65. package/types/SuiteTypes-DESBCvZ7.d.cts.map +0 -1
  66. package/types/SuiteTypes-cjI5BA_k.d.mts.map +0 -1
@@ -16,7 +16,7 @@ type Dumpable = {
16
16
 
17
17
  export class SuiteSerializer {
18
18
  static serialize(suite: Dumpable) {
19
- const dump = { ...suite.dump() };
19
+ const dump = stripMessageFromPassingTests(suite.dump());
20
20
 
21
21
  return IsolateSerializer.serialize(dump, suiteSerializerReplacer);
22
22
  }
@@ -50,6 +50,109 @@ export class SuiteSerializer {
50
50
  }
51
51
  }
52
52
 
53
+ function stripMessageFromPassingTests<T>(node: T): T {
54
+ const visited = new WeakMap<object, any>();
55
+ const containsPassingMemo = new WeakMap<object, boolean>();
56
+ const seen = new WeakSet<object>();
57
+
58
+ return strip(node, visited, containsPassingMemo, seen);
59
+ }
60
+
61
+ // eslint-disable-next-line complexity, max-statements
62
+ function strip<T>(
63
+ node: T,
64
+ visited: WeakMap<object, any>,
65
+ containsPassingMemo: WeakMap<object, boolean>,
66
+ seen: WeakSet<object>,
67
+ ): T {
68
+ if (!node || typeof node !== 'object') {
69
+ return node;
70
+ }
71
+
72
+ if (visited.has(node as object)) {
73
+ return visited.get(node as object);
74
+ }
75
+
76
+ if (!containsPassing(node, seen, containsPassingMemo)) {
77
+ return node;
78
+ }
79
+
80
+ if (Array.isArray(node)) {
81
+ const arr: any[] = [];
82
+ visited.set(node, arr);
83
+
84
+ node.forEach(value => {
85
+ arr.push(strip(value, visited, containsPassingMemo, seen));
86
+ });
87
+
88
+ return arr as T;
89
+ }
90
+
91
+ const root = node as Record<string, any>;
92
+ const shouldStripMessage = root.testStatus === TestStatus.PASSING;
93
+ const clonedNode: Record<string, any> = {};
94
+ visited.set(node, clonedNode);
95
+
96
+ for (const [key, value] of Object.entries(root)) {
97
+ if (shouldStripMessage && key === 'message') {
98
+ continue;
99
+ }
100
+
101
+ clonedNode[key] = strip(value, visited, containsPassingMemo, seen);
102
+ }
103
+
104
+ return clonedNode as T;
105
+ }
106
+
107
+ // `strip` calls this with a shared `seen` set that is expected to be empty
108
+ // at call boundaries. Inside this DFS, revisiting a node (`seen.has(node)`) means
109
+ // we've encountered a cycle currently in-flight, so we conservatively return `true`
110
+ // to avoid suppressing message-stripping while still preventing infinite recursion.
111
+ // This is safe because every path that adds to `seen` removes it (`seen.delete`) on
112
+ // return, and `memo` only stores final computed booleans for completed nodes.
113
+ // eslint-disable-next-line complexity, max-statements
114
+ function containsPassing(
115
+ node: unknown,
116
+ seen: WeakSet<object>,
117
+ memo: WeakMap<object, boolean>,
118
+ ): boolean {
119
+ if (!node || typeof node !== 'object') {
120
+ return false;
121
+ }
122
+
123
+ if (memo.has(node)) {
124
+ return memo.get(node) ?? false;
125
+ }
126
+
127
+ if (seen.has(node)) {
128
+ return true;
129
+ }
130
+
131
+ if ((node as Record<string, unknown>).testStatus === TestStatus.PASSING) {
132
+ memo.set(node, true);
133
+ return true;
134
+ }
135
+
136
+ seen.add(node);
137
+
138
+ const values = Array.isArray(node)
139
+ ? node
140
+ : Object.values(node as Record<string, unknown>);
141
+
142
+ for (const value of values) {
143
+ if (containsPassing(value, seen, memo)) {
144
+ memo.set(node, true);
145
+ seen.delete(node);
146
+ return true;
147
+ }
148
+ }
149
+
150
+ memo.set(node, false);
151
+ seen.delete(node);
152
+
153
+ return false;
154
+ }
155
+
53
156
  function suiteSerializerReplacer(value: any, key: string) {
54
157
  if (isStatusKey(key)) {
55
158
  return getAllowedStatus(value);
@@ -1,4 +1,4 @@
1
- import { describe, it, expect } from 'vitest';
1
+ import { describe, it, expect, expectTypeOf } from 'vitest';
2
2
 
3
3
  import { SuiteSerializer } from '../SuiteSerializer';
4
4
  import * as vest from '../../vest';
@@ -26,6 +26,35 @@ describe('SuiteSerializer', () => {
26
26
  const serialized = SuiteSerializer.serialize(suite);
27
27
  expect(serialized).toMatchSnapshot();
28
28
  });
29
+
30
+ it('should strip message from passing tests only', () => {
31
+ const suite = vest.create(() => {
32
+ vest.test('passing_field', 'passing_field_message', () => true);
33
+ vest.test('failing_field', 'failing_field_message', () => false);
34
+ });
35
+
36
+ suite.run();
37
+
38
+ const serialized = SuiteSerializer.serialize(suite);
39
+ const parsed = SuiteSerializer.deserialize(serialized);
40
+
41
+ expect(parsed.children).toBeDefined();
42
+
43
+ const testChildren = parsed.children?.filter(child => child.data.fieldName);
44
+ expect(testChildren).toHaveLength(2);
45
+
46
+ const passingTest = testChildren?.find(
47
+ child => child.data.fieldName === 'passing_field',
48
+ );
49
+ const failingTest = testChildren?.find(
50
+ child => child.data.fieldName === 'failing_field',
51
+ );
52
+
53
+ expect(passingTest).toBeDefined();
54
+ expect(failingTest).toBeDefined();
55
+ expect(passingTest?.data).not.toHaveProperty('message');
56
+ expect(failingTest?.data.message).toBe('failing_field_message');
57
+ });
29
58
  });
30
59
 
31
60
  describe('suite.resume', () => {
@@ -117,3 +146,31 @@ describe('suite.resume', () => {
117
146
  });
118
147
  });
119
148
  });
149
+
150
+ describe('SuiteSerializer type-compatibility', () => {
151
+ it('accepts schema-typed suites in resume()', () => {
152
+ const schema = vest.enforce.shape({
153
+ username: vest.enforce.isString(),
154
+ });
155
+
156
+ const suite = vest.create(data => {
157
+ vest.test('username', () => {
158
+ vest.enforce(data.username).isNotBlank();
159
+ });
160
+ }, schema);
161
+
162
+ suite.run({ username: '' });
163
+
164
+ const serialized = SuiteSerializer.serialize(suite);
165
+
166
+ const suite2 = vest.create(data => {
167
+ vest.test('username', () => {
168
+ vest.enforce(data.username).isNotBlank();
169
+ });
170
+ }, schema);
171
+
172
+ expectTypeOf(SuiteSerializer.resume).toBeFunction();
173
+ SuiteSerializer.resume(suite2, serialized);
174
+ expect(suite2.hasErrors('username')).toBe(true);
175
+ });
176
+ });
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, vi } from 'vitest';
1
+ import { describe, it, expect, expectTypeOf, vi } from 'vitest';
2
2
 
3
3
  import { dummyTest } from '../../testUtils/testDummy';
4
4
  import classnames from '../classnames';
@@ -60,6 +60,29 @@ describe('Utility: classnames', () => {
60
60
  expect(cn('travelerName')).toContain('invalid');
61
61
  });
62
62
 
63
+ it('accepts schema-typed suite results from suite.run() and preserves parser output type', () => {
64
+ const typedSuite = vest.create(
65
+ data => {
66
+ vest.test('travelerName', 'Traveler name is required', () => {
67
+ vest.enforce(data.travelerName).isNotBlank();
68
+ });
69
+ },
70
+ vest.enforce.shape({
71
+ travelerName: vest.enforce.isString(),
72
+ }),
73
+ );
74
+
75
+ const result = typedSuite.run({ travelerName: '' });
76
+
77
+ const cn = classnames(result, {
78
+ warning: 'warning',
79
+ invalid: 'invalid',
80
+ });
81
+
82
+ expectTypeOf(cn).toEqualTypeOf<(fieldName: string) => string>();
83
+ expect(cn('travelerName')).toContain('invalid');
84
+ });
85
+
63
86
  const suite = vest.create(() => {
64
87
  vest.mode(Modes.ALL);
65
88
  vest.skip('field_1');
@@ -1,4 +1,4 @@
1
- import { describe, it, expect } from 'vitest';
1
+ import { describe, it, expect, expectTypeOf } from 'vitest';
2
2
 
3
3
  import * as suiteDummy from '../../testUtils/suiteDummy';
4
4
  import { ser } from '../../testUtils/suiteDummy';
@@ -501,3 +501,25 @@ describe('parser.parse', () => {
501
501
  });
502
502
  });
503
503
  });
504
+
505
+ describe('parser.parse type-compatibility', () => {
506
+ it('accepts schema-driven suite.run() results and returns typed selectors', () => {
507
+ const suite = vest.create(
508
+ data => {
509
+ vest.test('email', () => {
510
+ vest.enforce(data.email).isString();
511
+ });
512
+ },
513
+ vest.enforce.shape({
514
+ email: vest.enforce.isString(),
515
+ }),
516
+ );
517
+
518
+ const parsed = parse(suite.run({ email: 'a@b.com' }));
519
+
520
+ expectTypeOf(parsed.valid).toEqualTypeOf<
521
+ (fieldName?: string | undefined) => boolean
522
+ >();
523
+ expect(parsed.valid('email')).toBe(true);
524
+ });
525
+ });
@@ -15,6 +15,16 @@ import { SuiteSelectors } from '../suiteResult/selectors/suiteSelectors';
15
15
 
16
16
  import { TTypedMethods } from './getTypedMethods';
17
17
 
18
+ export type SuiteRunData<
19
+ S extends TSchema,
20
+ T extends CB,
21
+ IsFocused extends boolean = false,
22
+ > = S extends undefined
23
+ ? Parameters<T>[0]
24
+ : IsFocused extends true
25
+ ? Partial<InferSchemaData<S>>
26
+ : InferSchemaData<S>;
27
+
18
28
  export type SuiteCallbackWithSchema<
19
29
  S extends TSchema,
20
30
  T extends CB,
@@ -47,17 +57,17 @@ type SuiteMethods<
47
57
  ...args: S extends undefined
48
58
  ? Parameters<T>
49
59
  : [data: InferSchemaData<S>, ...args: any[]]
50
- ) => SuiteResult<F, G>;
60
+ ) => SuiteResult<F, G, S, SuiteRunData<S, T>>;
51
61
  runStatic: (
52
62
  ...args: S extends undefined
53
63
  ? Parameters<T>
54
64
  : [data: InferSchemaData<S>, ...args: any[]]
55
- ) => SuiteResult<F, G>;
65
+ ) => SuiteResult<F, G, S, SuiteRunData<S, T>>;
56
66
  validate: (
57
67
  ...args: S extends undefined
58
68
  ? Parameters<T>
59
69
  : [data: InferSchemaData<S>, ...args: any[]]
60
- ) => SuiteResult<F, G>;
70
+ ) => SuiteResult<F, G, S, SuiteRunData<S, T>>;
61
71
  subscribe: Subscribe;
62
72
  } & AfterMethods<F, G, T, S> &
63
73
  TTypedMethods<F, G> &
@@ -85,7 +95,7 @@ type FocusedMethods<
85
95
  ...args: S extends undefined
86
96
  ? Parameters<T>
87
97
  : [data: Partial<InferSchemaData<S>>, ...args: any[]]
88
- ) => SuiteResult<F, G>;
98
+ ) => SuiteResult<F, G, S, SuiteRunData<S, T, true>>;
89
99
  };
90
100
 
91
101
  type AfterMethods<
@@ -108,7 +118,7 @@ type AfterMethods<
108
118
  ...args: S extends undefined
109
119
  ? Parameters<T>
110
120
  : [data: InferSchemaData<S>, ...args: any[]]
111
- ) => SuiteResult<F, G>;
121
+ ) => SuiteResult<F, G, S, SuiteRunData<S, T>>;
112
122
  };
113
123
 
114
124
  /**
@@ -0,0 +1,72 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { create } from '../../vest';
4
+
5
+ describe('suite result run summary metadata', () => {
6
+ it('stores the original run object and timestamp for non-schema suites', () => {
7
+ const payload = { name: 'alice', meta: { age: 33 } };
8
+ const suite = create((_data: any) => {});
9
+
10
+ const before = Date.now();
11
+ const result = suite.run(payload);
12
+ const after = Date.now();
13
+
14
+ expect(result.run.data).toBe(payload);
15
+ expect(Object.prototype.propertyIsEnumerable.call(result, 'run')).toBe(
16
+ false,
17
+ );
18
+ expect(Object.isFrozen(result.run)).toBe(true);
19
+ expect(result.run.time).toBeInstanceOf(Date);
20
+ expect(result.run.time.getTime()).toBeGreaterThanOrEqual(before);
21
+ expect(result.run.time.getTime()).toBeLessThanOrEqual(after);
22
+ });
23
+
24
+ it('stores parsed schema data in run metadata', () => {
25
+ const schema = {
26
+ parse: (value: any) => ({ amount: Number(value.amount) }),
27
+ run: (value: any) => ({ pass: true, type: value }),
28
+ } as any;
29
+
30
+ let callbackValue: unknown;
31
+ const suite = create((data: any) => {
32
+ callbackValue = data;
33
+ }, schema);
34
+
35
+ const result = suite.run({ amount: '12' } as any);
36
+
37
+ expect(result.run.data).toEqual({ amount: 12 });
38
+ expect(callbackValue).toEqual(result.run.data);
39
+ });
40
+
41
+ it('falls back to the original data in run metadata when parse fails', () => {
42
+ const schema = {
43
+ parse: () => {
44
+ throw new TypeError('parse failed');
45
+ },
46
+ run: (value: any) => ({
47
+ pass: true,
48
+ type: value,
49
+ }),
50
+ } as any;
51
+
52
+ const payload = { amount: '12' };
53
+ const suite = create(() => {}, schema);
54
+
55
+ const result = suite.run(payload);
56
+
57
+ expect(result.run.data).toBe(payload);
58
+ });
59
+
60
+ it('captures run.time per run', () => {
61
+ const suite = create((_data: any) => {});
62
+
63
+ const first = suite.run({ count: 1 });
64
+ const second = suite.run({ count: 2 });
65
+
66
+ expect(second.run.time.getTime()).toBeGreaterThanOrEqual(
67
+ first.run.time.getTime(),
68
+ );
69
+ expect(first.run.data).toEqual({ count: 1 });
70
+ expect(second.run.data).toEqual({ count: 2 });
71
+ });
72
+ });
@@ -33,6 +33,10 @@ describe('schema driven suite types', () => {
33
33
 
34
34
  suite.run({ name: 'john', age: 42 });
35
35
 
36
+ expectTypeOf(suite.run({ name: 'jane', age: 30 }).run.data).toEqualTypeOf<
37
+ { name: string; age: number } | undefined
38
+ >();
39
+
36
40
  void (0 as unknown as AssertTrue<
37
41
  IsEqual<
38
42
  ReturnType<typeof suite.get>['types']['output'],
@@ -140,6 +144,10 @@ describe('schema driven suite types', () => {
140
144
 
141
145
  suite.run(10, true);
142
146
 
147
+ expectTypeOf(suite.run(10, true).run.data).toEqualTypeOf<
148
+ number | undefined
149
+ >();
150
+
143
151
  void (0 as unknown as AssertTrue<
144
152
  IsEqual<ReturnType<typeof suite.get>['types'], undefined>
145
153
  >);
@@ -158,6 +166,10 @@ describe('schema driven suite types', () => {
158
166
 
159
167
  suite.run({ name: 'john', age: 42 });
160
168
 
169
+ expectTypeOf(suite.run({ name: 'john', age: 42 }).run.data).toEqualTypeOf<
170
+ { name: string; age: number } | undefined
171
+ >();
172
+
161
173
  expectTypeOf(suite.get().types).toBeUndefined();
162
174
 
163
175
  // @ts-expect-error - requires an argument
@@ -24,7 +24,11 @@ import {
24
24
  } from '../suiteResult/SuiteResultTypes';
25
25
  import { useCreateSuiteResult } from '../suiteResult/suiteResult';
26
26
 
27
- import { SuiteModifiers, SuiteCallbackWithSchema } from './SuiteTypes';
27
+ import {
28
+ SuiteModifiers,
29
+ SuiteCallbackWithSchema,
30
+ SuiteRunData,
31
+ } from './SuiteTypes';
28
32
 
29
33
  type SchemaRunResult = {
30
34
  readonly message?: string;
@@ -56,8 +60,10 @@ export function useCreateSuiteRunner<
56
60
  ...args: S extends undefined
57
61
  ? Parameters<T>
58
62
  : [data: InferSchemaData<S>, ...args: any[]]
59
- ): SuiteResult<F, G, S> {
60
- const { resolve, promise } = withResolvers<SuiteResult<F, G, S>>();
63
+ ): SuiteResult<F, G, S, SuiteRunData<S, T>> {
64
+ const runTime = new Date();
65
+ const { resolve, promise } =
66
+ withResolvers<SuiteResult<F, G, S, SuiteRunData<S, T>>>();
61
67
 
62
68
  const schemaInput = args[0];
63
69
  const schemaRunResult = shouldRunSchema(schema, transformedModifiers)
@@ -66,46 +72,59 @@ export function useCreateSuiteRunner<
66
72
 
67
73
  const callbackInput = getCallbackInput(schemaRunResult, schemaInput);
68
74
  const callbackArgs = [callbackInput, ...args.slice(1)] as Parameters<T>;
75
+ const runData = callbackInput as SuiteRunData<S, T> | undefined;
69
76
 
70
- return assign(
71
- promise,
72
- SuiteContext.run(
73
- {
74
- suiteParams: callbackArgs,
75
- schema,
76
- modifiers: transformedModifiers,
77
- },
78
- () => {
79
- useEmit('SUITE_RUN_STARTED');
80
-
81
- const useResolver = () => {
82
- const result = useCreateSuiteResult<F, G, S>(
83
- schema,
84
- callbackInput,
85
- schemaInput,
86
- );
87
-
88
- if (!result.isPending()) {
89
- resolve(result);
90
- }
91
-
92
- return result;
93
- };
94
-
95
- return IsolateSuite(
96
- useRunSuiteCallback<F, T, S>({
97
- args: callbackArgs,
98
- modifiers: transformedModifiers,
99
- schema,
100
- schemaRunResult,
101
- suiteCallback,
102
- useResolver,
103
- }),
77
+ const suiteResult = SuiteContext.run(
78
+ {
79
+ suiteParams: callbackArgs,
80
+ schema,
81
+ modifiers: transformedModifiers,
82
+ },
83
+ () => {
84
+ useEmit('SUITE_RUN_STARTED');
85
+
86
+ const useResolver = () => {
87
+ const result = useCreateSuiteResult<F, G, S, SuiteRunData<S, T>>(
88
+ schema,
89
+ callbackInput,
90
+ runData,
91
+ runTime,
92
+ );
93
+
94
+ if (!result.isPending()) {
95
+ resolve(result);
96
+ }
97
+
98
+ return result;
99
+ };
100
+
101
+ return IsolateSuite(
102
+ useRunSuiteCallback<F, T, S, G, SuiteRunData<S, T>>({
103
+ args: callbackArgs,
104
+ modifiers: transformedModifiers,
105
+ schema,
106
+ schemaRunResult,
107
+ suiteCallback,
104
108
  useResolver,
105
- ).output;
106
- },
107
- ),
109
+ }),
110
+ useResolver,
111
+ ).output;
112
+ },
108
113
  );
114
+
115
+ const result = assign(promise, suiteResult);
116
+
117
+ Object.defineProperty(result, 'run', {
118
+ configurable: true,
119
+ enumerable: false,
120
+ value: Object.freeze({
121
+ data: runData,
122
+ time: runTime,
123
+ }),
124
+ writable: true,
125
+ });
126
+
127
+ return result;
109
128
  };
110
129
  }
111
130
 
@@ -131,13 +150,15 @@ function useRunSuiteCallback<
131
150
  F extends TFieldName,
132
151
  T extends CB = CB,
133
152
  S extends TSchema = undefined,
153
+ G extends TGroupName = TGroupName,
154
+ D = unknown,
134
155
  >(params: {
135
156
  args: any[];
136
157
  modifiers: ReturnType<typeof useTransformedModifiers<F>>;
137
158
  schema: S | undefined;
138
159
  schemaRunResult?: SchemaRunResult[];
139
160
  suiteCallback: SuiteCallbackWithSchema<S, T>;
140
- useResolver: () => SuiteResult<F, any, S>;
161
+ useResolver: () => SuiteResult<F, G, S, D>;
141
162
  }) {
142
163
  const {
143
164
  args,
@@ -17,12 +17,28 @@ export class SummaryBase {
17
17
  export class SuiteSummary<
18
18
  F extends TFieldName,
19
19
  G extends TGroupName,
20
+ D = unknown,
20
21
  > extends SummaryBase {
21
22
  public [Severity.ERRORS]: SummaryFailure<F, G>[] = [];
22
23
  public [Severity.WARNINGS]: SummaryFailure<F, G>[] = [];
23
24
  public groups: Groups<G, F> = {} as Groups<G, F>;
24
25
  public tests: Tests<F> = {} as Tests<F>;
26
+ public run!: { data: D | undefined; time: Date };
25
27
  public valid: Nullable<boolean> = null;
28
+
29
+ constructor() {
30
+ super();
31
+
32
+ Object.defineProperty(this, 'run', {
33
+ configurable: true,
34
+ enumerable: false,
35
+ value: {
36
+ data: undefined,
37
+ time: new Date(0),
38
+ },
39
+ writable: true,
40
+ });
41
+ }
26
42
  }
27
43
 
28
44
  export type TestsContainer<F extends TFieldName, _G extends TGroupName> =
@@ -75,20 +91,21 @@ type SuiteResultData<
75
91
  F extends TFieldName,
76
92
  G extends TGroupName,
77
93
  S extends TSchema = undefined,
94
+ D = unknown,
78
95
  > =
79
- | (Omit<SuiteSummary<F, G>, 'valid'> &
96
+ | (Omit<SuiteSummary<F, G, D>, 'valid'> &
80
97
  SuiteSelectors<F, G> & {
81
98
  valid: true;
82
99
  value: InferSchemaOutput<S>;
83
100
  issues?: undefined;
84
101
  })
85
- | (Omit<SuiteSummary<F, G>, 'valid'> &
102
+ | (Omit<SuiteSummary<F, G, D>, 'valid'> &
86
103
  SuiteSelectors<F, G> & {
87
104
  valid: false;
88
105
  issues: ReadonlyArray<StandardSchemaV1.Issue>;
89
106
  value?: undefined;
90
107
  })
91
- | (Omit<SuiteSummary<F, G>, 'valid'> &
108
+ | (Omit<SuiteSummary<F, G, D>, 'valid'> &
92
109
  SuiteSelectors<F, G> & {
93
110
  valid: null;
94
111
  issues?: undefined;
@@ -102,7 +119,8 @@ export type SuiteResult<
102
119
  F extends string = TFieldName,
103
120
  G extends string = TGroupName,
104
121
  S extends TSchema = undefined,
105
- > = SuiteResultData<BrandedFieldName<F>, BrandedGroupName<G>, S> & {
122
+ D = unknown,
123
+ > = SuiteResultData<BrandedFieldName<F>, BrandedGroupName<G>, S, D> & {
106
124
  dump: CB<TIsolateSuite>;
107
125
  types: S extends undefined
108
126
  ? undefined
@@ -26,10 +26,11 @@ import {
26
26
  export function useProduceSuiteSummary<
27
27
  F extends TFieldName,
28
28
  G extends TGroupName,
29
- >(): SuiteSummary<F, G> {
29
+ D = unknown,
30
+ >(): SuiteSummary<F, G, D> {
30
31
  const root = VestRuntime.useAvailableRoot<TIsolateSuite>();
31
32
 
32
- const summary = new SuiteSummary<F, G>();
33
+ const summary = new SuiteSummary<F, G, D>();
33
34
 
34
35
  if (isVestIsolate(root)) {
35
36
  useProcessTests(root.data.tests, summary);