UNPKG

5.51 kBJavaScriptView Raw
1import tap from 'zora-tap-reporter';
2import deepEqual from 'deep-equal';
3
4const getAssertionLocation = () => {
5 const err = new Error();
6 const stack = (err.stack || '').split('\n');
7 return (stack[3] || '').trim().replace(/^at/i, '');
8};
9const assertMethodHook = fn => function (...args) {
10 const assertResult = fn(...args);
11
12 if (assertResult.pass === false) {
13 assertResult.at = getAssertionLocation();
14 }
15
16 this.collect(assertResult);
17 return assertResult;
18};
19
20const Assertion = {
21 ok: assertMethodHook((val, message = 'should be truthy') => ({
22 pass: Boolean(val),
23 actual: val,
24 expected: true,
25 message,
26 operator: 'ok'
27 })),
28 deepEqual: assertMethodHook((actual, expected, message = 'should be equivalent') => ({
29 pass: deepEqual(actual, expected),
30 actual,
31 expected,
32 message,
33 operator: 'deepEqual'
34 })),
35 equal: assertMethodHook((actual, expected, message = 'should be equal') => ({
36 pass: actual === expected,
37 actual,
38 expected,
39 message,
40 operator: 'equal'
41 })),
42 notOk: assertMethodHook((val, message = 'should not be truthy') => ({
43 pass: !val,
44 expected: false,
45 actual: val,
46 message,
47 operator: 'notOk'
48 })),
49 notDeepEqual: assertMethodHook((actual, expected, message = 'should not be equivalent') => ({
50 pass: !deepEqual(actual, expected),
51 actual,
52 expected,
53 message,
54 operator: 'notDeepEqual'
55 })),
56 notEqual: assertMethodHook((actual, expected, message = 'should not be equal') => ({
57 pass: actual !== expected,
58 actual,
59 expected,
60 message,
61 operator: 'notEqual'
62 })),
63 throws: assertMethodHook((func, expected, message) => {
64 let caught;
65 let pass;
66 let actual;
67 if (typeof expected === 'string') {
68 [expected, message] = [message, expected];
69 }
70 try {
71 func();
72 } catch (err) {
73 caught = {error: err};
74 }
75 pass = caught !== undefined;
76 actual = caught && caught.error;
77 if (expected instanceof RegExp) {
78 pass = expected.test(actual) || expected.test(actual && actual.message);
79 expected = String(expected);
80 } else if (typeof expected === 'function' && caught) {
81 pass = actual instanceof expected;
82 actual = actual.constructor;
83 }
84 return {
85 pass,
86 expected,
87 actual,
88 operator: 'throws',
89 message: message || 'should throw'
90 };
91 }),
92 doesNotThrow: assertMethodHook((func, expected, message) => {
93 let caught;
94 if (typeof expected === 'string') {
95 [expected, message] = [message, expected];
96 }
97 try {
98 func();
99 } catch (err) {
100 caught = {error: err};
101 }
102 return {
103 pass: caught === undefined,
104 expected: 'no thrown error',
105 actual: caught && caught.error,
106 operator: 'doesNotThrow',
107 message: message || 'should not throw'
108 };
109 }),
110 fail: assertMethodHook((message = 'fail called') => ({
111 pass: false,
112 actual: 'fail called',
113 expected: 'fail not called',
114 message,
115 operator: 'fail'
116 }))
117};
118
119var assert = collect => Object.create(Assertion, {collect: {value: collect}});
120
121const noop = () => {};
122
123const skip = description => test('SKIPPED - ' + description, noop);
124
125const Test = {
126 async run() {
127 const collect = assertion => this.items.push(assertion);
128 const start = Date.now();
129 await Promise.resolve(this.spec(assert(collect)));
130 const executionTime = Date.now() - start;
131 return Object.assign(this, {
132 executionTime
133 });
134 },
135 skip() {
136 return skip(this.description);
137 }
138};
139
140function test(description, spec, {only = false} = {}) {
141 return Object.create(Test, {
142 items: {value: []},
143 only: {value: only},
144 spec: {value: spec},
145 description: {value: description}
146 });
147}
148
149// Force to resolve on next tick so consumer can do something with previous iteration result
150const onNextTick = val => new Promise(resolve => setTimeout(() => resolve(val), 0));
151
152const PlanProto = {
153 [Symbol.iterator]() {
154 return this.items[Symbol.iterator]();
155 },
156 test(description, spec, opts) {
157 if (!spec && description.test) {
158 // If it is a plan
159 this.items.push(...description);
160 } else {
161 this.items.push(test(description, spec, opts));
162 }
163 return this;
164 },
165 only(description, spec) {
166 return this.test(description, spec, {only: true});
167 },
168 skip(description, spec) {
169 if (!spec && description.test) {
170 // If it is a plan we skip the whole plan
171 for (const t of description) {
172 this.items.push(t.skip());
173 }
174 } else {
175 this.items.push(skip(description));
176 }
177 return this;
178 }
179};
180
181const runnify = fn => async function (sink = tap()) {
182 const sinkIterator = typeof sink[Symbol.iterator] === 'function' ?
183 sink[Symbol.iterator]() :
184 sink(); // Backward compatibility
185 sinkIterator.next();
186 try {
187 const hasOnly = this.items.some(t => t.only);
188 const tests = hasOnly ? this.items.map(t => t.only ? t : t.skip()) : this.items;
189 await fn(tests, sinkIterator);
190 } catch (err) {
191 sinkIterator.throw(err);
192 } finally {
193 sinkIterator.return();
194 }
195};
196
197function factory({sequence = false} = {sequence: false}) {
198 /* eslint-disable no-await-in-loop */
199 const exec = sequence === true ? async (tests, sinkIterator) => {
200 for (const t of tests) {
201 const result = await onNextTick(t.run());
202 sinkIterator.next(result);
203 }
204 } : async (tests, sinkIterator) => {
205 const runningTests = tests.map(t => t.run());
206 for (const r of runningTests) {
207 const executedTest = await onNextTick(r);
208 sinkIterator.next(executedTest);
209 }
210 };
211 /* eslint-enable no-await-in-loop */
212
213 return Object.assign(Object.create(PlanProto, {
214 items: {value: []}, length: {
215 get() {
216 return this.items.length;
217 }
218 }
219 }), {
220 run: runnify(exec)
221 });
222}
223
224export default factory;